@awcp/sdk 0.0.16 → 0.0.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/delegator/admission.d.ts +7 -28
- package/dist/delegator/admission.d.ts.map +1 -1
- package/dist/delegator/admission.js +35 -26
- package/dist/delegator/admission.js.map +1 -1
- package/dist/delegator/bin/client.d.ts +2 -2
- package/dist/delegator/bin/client.d.ts.map +1 -1
- package/dist/delegator/bin/client.js +6 -2
- package/dist/delegator/bin/client.js.map +1 -1
- package/dist/delegator/bin/daemon.d.ts +1 -1
- package/dist/delegator/bin/daemon.d.ts.map +1 -1
- package/dist/delegator/bin/daemon.js +13 -4
- package/dist/delegator/bin/daemon.js.map +1 -1
- package/dist/delegator/config.d.ts +57 -78
- package/dist/delegator/config.d.ts.map +1 -1
- package/dist/delegator/config.js +40 -24
- package/dist/delegator/config.js.map +1 -1
- package/dist/delegator/delegation-manager.d.ts +12 -0
- package/dist/delegator/delegation-manager.d.ts.map +1 -0
- package/dist/delegator/delegation-manager.js +36 -0
- package/dist/delegator/delegation-manager.js.map +1 -0
- package/dist/delegator/{environment-builder.d.ts → environment-manager.d.ts} +6 -12
- package/dist/delegator/{environment-builder.d.ts.map → environment-manager.d.ts.map} +1 -1
- package/dist/delegator/{environment-builder.js → environment-manager.js} +8 -26
- package/dist/delegator/environment-manager.js.map +1 -0
- package/dist/delegator/executor-client.d.ts +8 -25
- package/dist/delegator/executor-client.d.ts.map +1 -1
- package/dist/delegator/executor-client.js +33 -53
- package/dist/delegator/executor-client.js.map +1 -1
- package/dist/delegator/index.d.ts +5 -4
- package/dist/delegator/index.d.ts.map +1 -1
- package/dist/delegator/index.js +3 -2
- package/dist/delegator/index.js.map +1 -1
- package/dist/delegator/service.d.ts +28 -14
- package/dist/delegator/service.d.ts.map +1 -1
- package/dist/delegator/service.js +258 -297
- package/dist/delegator/service.js.map +1 -1
- package/dist/delegator/snapshot-manager.d.ts +23 -0
- package/dist/delegator/snapshot-manager.d.ts.map +1 -0
- package/dist/delegator/snapshot-manager.js +120 -0
- package/dist/delegator/snapshot-manager.js.map +1 -0
- package/dist/executor/a2a-adapter.d.ts +1 -1
- package/dist/executor/a2a-adapter.d.ts.map +1 -1
- package/dist/executor/admission.d.ts +13 -0
- package/dist/executor/admission.d.ts.map +1 -0
- package/dist/executor/admission.js +27 -0
- package/dist/executor/admission.js.map +1 -0
- package/dist/executor/assignment-manager.d.ts +12 -0
- package/dist/executor/assignment-manager.d.ts.map +1 -0
- package/dist/executor/assignment-manager.js +36 -0
- package/dist/executor/assignment-manager.js.map +1 -0
- package/dist/executor/config.d.ts +37 -24
- package/dist/executor/config.d.ts.map +1 -1
- package/dist/executor/config.js +21 -15
- package/dist/executor/config.js.map +1 -1
- package/dist/executor/index.d.ts +4 -2
- package/dist/executor/index.d.ts.map +1 -1
- package/dist/executor/index.js +3 -1
- package/dist/executor/index.js.map +1 -1
- package/dist/executor/service.d.ts +18 -12
- package/dist/executor/service.d.ts.map +1 -1
- package/dist/executor/service.js +325 -211
- package/dist/executor/service.js.map +1 -1
- package/dist/executor/workspace-manager.d.ts +1 -8
- package/dist/executor/workspace-manager.d.ts.map +1 -1
- package/dist/executor/workspace-manager.js +3 -25
- package/dist/executor/workspace-manager.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/listener/http-listener.d.ts +1 -1
- package/dist/listener/http-listener.d.ts.map +1 -1
- package/dist/listener/http-listener.js +8 -1
- package/dist/listener/http-listener.js.map +1 -1
- package/dist/listener/index.d.ts +1 -0
- package/dist/listener/index.d.ts.map +1 -1
- package/dist/listener/index.js.map +1 -1
- package/dist/listener/types.d.ts +52 -0
- package/dist/listener/types.d.ts.map +1 -0
- package/dist/listener/types.js +5 -0
- package/dist/listener/types.js.map +1 -0
- package/dist/listener/websocket-tunnel-listener.d.ts +1 -1
- package/dist/listener/websocket-tunnel-listener.d.ts.map +1 -1
- package/dist/server/express/awcp-executor-handler.d.ts +3 -3
- package/dist/server/express/awcp-executor-handler.d.ts.map +1 -1
- package/dist/server/express/awcp-executor-handler.js +3 -1
- package/dist/server/express/awcp-executor-handler.js.map +1 -1
- package/dist/utils/fs-helpers.d.ts +1 -1
- package/dist/utils/fs-helpers.js +1 -1
- package/package.json +2 -2
- package/dist/delegator/environment-builder.js.map +0 -1
- package/dist/delegator/snapshot-store.d.ts +0 -27
- package/dist/delegator/snapshot-store.d.ts.map +0 -1
- package/dist/delegator/snapshot-store.js +0 -40
- package/dist/delegator/snapshot-store.js.map +0 -1
|
@@ -5,167 +5,289 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from 'node:fs/promises';
|
|
7
7
|
import * as path from 'node:path';
|
|
8
|
-
import { DelegationStateMachine,
|
|
8
|
+
import { DelegationStateMachine, isTerminalState, createDelegation, generateDelegationId, PROTOCOL_VERSION, AwcpError, WorkspaceNotFoundError, WorkspaceInvalidError, } from '@awcp/core';
|
|
9
9
|
import { resolveDelegatorConfig } from './config.js';
|
|
10
10
|
import { AdmissionController } from './admission.js';
|
|
11
|
-
import {
|
|
11
|
+
import { DelegationManager } from './delegation-manager.js';
|
|
12
|
+
import { EnvironmentManager } from './environment-manager.js';
|
|
12
13
|
import { ExecutorClient } from './executor-client.js';
|
|
13
|
-
import {
|
|
14
|
+
import { SnapshotManager } from './snapshot-manager.js';
|
|
14
15
|
export class DelegatorService {
|
|
15
16
|
config;
|
|
16
17
|
transport;
|
|
17
18
|
admissionController;
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
delegationManager;
|
|
20
|
+
environmentManager;
|
|
21
|
+
snapshotManager;
|
|
20
22
|
executorClient;
|
|
21
23
|
delegations = new Map();
|
|
22
24
|
stateMachines = new Map();
|
|
23
|
-
executorUrls = new Map();
|
|
24
25
|
cleanupTimer;
|
|
25
26
|
constructor(options) {
|
|
26
27
|
this.config = resolveDelegatorConfig(options.config);
|
|
27
28
|
this.transport = this.config.transport;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this.config.snapshot.mode = 'auto';
|
|
29
|
+
if (this.transport.capabilities.liveSync && this.config.delegation.snapshot.mode === 'staged') {
|
|
30
|
+
this.config.delegation.snapshot.mode = 'auto';
|
|
31
31
|
}
|
|
32
|
-
this.admissionController = new AdmissionController(
|
|
33
|
-
|
|
34
|
-
maxFileCount: this.config.admission.maxFileCount,
|
|
35
|
-
maxSingleFileBytes: this.config.admission.maxSingleFileBytes,
|
|
36
|
-
});
|
|
37
|
-
this.environmentBuilder = new EnvironmentBuilder({
|
|
32
|
+
this.admissionController = new AdmissionController(this.config.admission);
|
|
33
|
+
this.delegationManager = new DelegationManager({
|
|
38
34
|
baseDir: path.join(this.config.baseDir, 'delegations'),
|
|
39
35
|
});
|
|
40
|
-
this.
|
|
41
|
-
baseDir: this.config.baseDir,
|
|
36
|
+
this.environmentManager = new EnvironmentManager({
|
|
37
|
+
baseDir: path.join(this.config.baseDir, 'environments'),
|
|
38
|
+
});
|
|
39
|
+
this.snapshotManager = new SnapshotManager({
|
|
40
|
+
baseDir: path.join(this.config.baseDir, 'snapshots'),
|
|
41
|
+
transport: this.transport,
|
|
42
42
|
});
|
|
43
|
-
|
|
43
|
+
const { requestTimeout, sseMaxRetries, sseRetryDelayMs } = this.config.delegation.connection;
|
|
44
|
+
this.executorClient = new ExecutorClient(requestTimeout, sseMaxRetries, sseRetryDelayMs);
|
|
44
45
|
this.startCleanupTimer();
|
|
45
46
|
}
|
|
47
|
+
async initialize() {
|
|
48
|
+
await this.transport.initialize?.();
|
|
49
|
+
if (this.config.cleanupOnInitialize) {
|
|
50
|
+
const persistedDelegations = await this.delegationManager.loadAll();
|
|
51
|
+
for (const delegation of persistedDelegations) {
|
|
52
|
+
await this.transport.release(delegation.id).catch(() => { });
|
|
53
|
+
await this.environmentManager.release(delegation.id);
|
|
54
|
+
await this.snapshotManager.cleanupDelegation(delegation.id);
|
|
55
|
+
await this.delegationManager.delete(delegation.id).catch(() => { });
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const persistedDelegations = await this.delegationManager.loadAll();
|
|
60
|
+
const knownIds = new Set(persistedDelegations.map(d => d.id));
|
|
61
|
+
await this.environmentManager.cleanupStale(knownIds);
|
|
62
|
+
await this.snapshotManager.cleanupStale(knownIds);
|
|
63
|
+
for (const delegation of persistedDelegations) {
|
|
64
|
+
this.delegations.set(delegation.id, delegation);
|
|
65
|
+
this.stateMachines.set(delegation.id, new DelegationStateMachine(delegation.state));
|
|
66
|
+
}
|
|
67
|
+
for (const delegation of persistedDelegations) {
|
|
68
|
+
if (isTerminalState(delegation.state))
|
|
69
|
+
continue;
|
|
70
|
+
console.log(`[AWCP:Delegator] Auto-resuming delegation ${delegation.id} (state=${delegation.state})`);
|
|
71
|
+
try {
|
|
72
|
+
await this.delegate({
|
|
73
|
+
existingId: delegation.id,
|
|
74
|
+
executorUrl: delegation.peerUrl,
|
|
75
|
+
environment: delegation.environment,
|
|
76
|
+
task: delegation.task,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.error(`[AWCP:Delegator] Failed to resume ${delegation.id}:`, error instanceof Error ? error.message : error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async shutdown() {
|
|
85
|
+
if (this.cleanupTimer) {
|
|
86
|
+
clearInterval(this.cleanupTimer);
|
|
87
|
+
this.cleanupTimer = undefined;
|
|
88
|
+
}
|
|
89
|
+
await this.transport.shutdown?.();
|
|
90
|
+
this.delegations.clear();
|
|
91
|
+
this.stateMachines.clear();
|
|
92
|
+
}
|
|
46
93
|
async delegate(params) {
|
|
47
|
-
const
|
|
94
|
+
const isResume = !!params.existingId;
|
|
95
|
+
const delegationId = params.existingId ?? generateDelegationId();
|
|
96
|
+
if (isResume) {
|
|
97
|
+
const existing = this.delegations.get(delegationId);
|
|
98
|
+
if (!existing) {
|
|
99
|
+
throw new Error(`Cannot resume unknown delegation: ${delegationId}` +
|
|
100
|
+
` (known=[${Array.from(this.delegations.keys()).join(',')}])`);
|
|
101
|
+
}
|
|
102
|
+
if (isTerminalState(existing.state)) {
|
|
103
|
+
throw new Error(`Cannot resume terminal delegation: ${delegationId} (state=${existing.state})`);
|
|
104
|
+
}
|
|
105
|
+
await this.transport.detach(delegationId).catch(() => { });
|
|
106
|
+
this.stateMachines.set(delegationId, new DelegationStateMachine());
|
|
107
|
+
}
|
|
48
108
|
for (const resource of params.environment.resources) {
|
|
49
109
|
const sourcePath = await this.validateAndNormalizePath(resource.source, delegationId);
|
|
50
110
|
resource.source = sourcePath;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
throw new WorkspaceTooLargeError(admissionResult.stats ?? {}, admissionResult.hint, delegationId);
|
|
54
|
-
}
|
|
111
|
+
await this.admissionController.check(sourcePath, delegationId);
|
|
112
|
+
await this.config.hooks.onAdmissionCheck?.(sourcePath);
|
|
55
113
|
}
|
|
56
|
-
const ttlSeconds = params.ttlSeconds ?? this.config.
|
|
57
|
-
const accessMode = params.accessMode ?? this.config.
|
|
58
|
-
const snapshotMode = params.snapshotMode ?? this.config.snapshot.mode;
|
|
59
|
-
const
|
|
60
|
-
id: delegationId,
|
|
61
|
-
peerUrl: params.executorUrl,
|
|
62
|
-
environment: params.environment,
|
|
63
|
-
task: params.task,
|
|
64
|
-
leaseConfig: { ttlSeconds, accessMode },
|
|
65
|
-
});
|
|
66
|
-
delegation.snapshotPolicy = {
|
|
67
|
-
mode: snapshotMode,
|
|
68
|
-
retentionMs: this.config.snapshot.retentionMs,
|
|
69
|
-
maxSnapshots: this.config.snapshot.maxSnapshots,
|
|
70
|
-
};
|
|
71
|
-
const { envRoot } = await this.environmentBuilder.build(delegationId, params.environment);
|
|
72
|
-
delegation.exportPath = envRoot;
|
|
73
|
-
const stateMachine = new DelegationStateMachine();
|
|
74
|
-
this.delegations.set(delegationId, delegation);
|
|
75
|
-
this.stateMachines.set(delegationId, stateMachine);
|
|
76
|
-
this.executorUrls.set(delegationId, params.executorUrl);
|
|
77
|
-
const inviteMessage = {
|
|
78
|
-
version: PROTOCOL_VERSION,
|
|
79
|
-
type: 'INVITE',
|
|
80
|
-
delegationId,
|
|
81
|
-
task: params.task,
|
|
82
|
-
lease: { ttlSeconds, accessMode },
|
|
83
|
-
environment: {
|
|
84
|
-
resources: params.environment.resources.map(r => ({
|
|
85
|
-
name: r.name,
|
|
86
|
-
type: r.type,
|
|
87
|
-
mode: r.mode,
|
|
88
|
-
})),
|
|
89
|
-
},
|
|
90
|
-
requirements: {
|
|
91
|
-
transport: this.transport.type,
|
|
92
|
-
},
|
|
93
|
-
...(params.auth && { auth: params.auth }),
|
|
94
|
-
};
|
|
95
|
-
this.transitionState(delegationId, { type: 'SEND_INVITE', message: inviteMessage });
|
|
114
|
+
const ttlSeconds = params.ttlSeconds ?? this.config.delegation.lease.ttlSeconds;
|
|
115
|
+
const accessMode = params.accessMode ?? this.config.delegation.lease.accessMode;
|
|
116
|
+
const snapshotMode = params.snapshotMode ?? this.config.delegation.snapshot.mode;
|
|
117
|
+
const retentionMs = params.retentionMs ?? this.config.delegation.retentionMs;
|
|
96
118
|
try {
|
|
119
|
+
const { envRoot } = await this.environmentManager.build(delegationId, params.environment);
|
|
120
|
+
if (isResume) {
|
|
121
|
+
const delegation = this.delegations.get(delegationId);
|
|
122
|
+
delegation.state = 'created';
|
|
123
|
+
delegation.exportPath = envRoot;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const delegation = createDelegation({
|
|
127
|
+
id: delegationId,
|
|
128
|
+
peerUrl: params.executorUrl,
|
|
129
|
+
environment: params.environment,
|
|
130
|
+
task: params.task,
|
|
131
|
+
leaseConfig: { ttlSeconds, accessMode },
|
|
132
|
+
retentionMs,
|
|
133
|
+
snapshotPolicy: {
|
|
134
|
+
mode: snapshotMode,
|
|
135
|
+
maxSnapshots: this.config.delegation.snapshot.maxSnapshots,
|
|
136
|
+
},
|
|
137
|
+
exportPath: envRoot,
|
|
138
|
+
});
|
|
139
|
+
this.delegations.set(delegationId, delegation);
|
|
140
|
+
this.stateMachines.set(delegationId, new DelegationStateMachine());
|
|
141
|
+
}
|
|
142
|
+
const inviteMessage = {
|
|
143
|
+
version: PROTOCOL_VERSION,
|
|
144
|
+
type: 'INVITE',
|
|
145
|
+
delegationId,
|
|
146
|
+
task: params.task,
|
|
147
|
+
lease: { ttlSeconds, accessMode },
|
|
148
|
+
retentionMs,
|
|
149
|
+
environment: {
|
|
150
|
+
resources: params.environment.resources.map(r => ({
|
|
151
|
+
name: r.name,
|
|
152
|
+
type: r.type,
|
|
153
|
+
mode: r.mode,
|
|
154
|
+
})),
|
|
155
|
+
},
|
|
156
|
+
requirements: {
|
|
157
|
+
transport: this.transport.type,
|
|
158
|
+
},
|
|
159
|
+
...(params.auth && { auth: params.auth }),
|
|
160
|
+
};
|
|
161
|
+
this.transitionState(delegationId, { type: 'SEND_INVITE', message: inviteMessage });
|
|
97
162
|
const response = await this.executorClient.sendInvite(params.executorUrl, inviteMessage);
|
|
98
163
|
if (response.type === 'ERROR') {
|
|
99
164
|
await this.handleError(response);
|
|
100
165
|
throw new AwcpError(response.code, response.message, response.hint, delegationId);
|
|
101
166
|
}
|
|
102
167
|
await this.handleAccept(response);
|
|
103
|
-
this.config.hooks.onDelegationCreated?.(
|
|
168
|
+
this.config.hooks.onDelegationCreated?.(this.delegations.get(delegationId));
|
|
104
169
|
return delegationId;
|
|
105
170
|
}
|
|
106
171
|
catch (error) {
|
|
107
|
-
|
|
172
|
+
if (isResume) {
|
|
173
|
+
await this.transport.detach(delegationId).catch(() => { });
|
|
174
|
+
await this.environmentManager.release(delegationId);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
this.delegations.delete(delegationId);
|
|
178
|
+
this.stateMachines.delete(delegationId);
|
|
179
|
+
await this.transport.release(delegationId).catch(() => { });
|
|
180
|
+
await this.environmentManager.release(delegationId);
|
|
181
|
+
await this.delegationManager.delete(delegationId).catch(() => { });
|
|
182
|
+
}
|
|
108
183
|
throw error;
|
|
109
184
|
}
|
|
110
185
|
}
|
|
111
186
|
async handleAccept(message) {
|
|
112
187
|
const delegation = this.delegations.get(message.delegationId);
|
|
113
188
|
if (!delegation) {
|
|
114
|
-
|
|
115
|
-
|
|
189
|
+
throw new Error(`Unknown delegation for ACCEPT: ${message.delegationId}` +
|
|
190
|
+
` (known=[${Array.from(this.delegations.keys()).join(',')}])`);
|
|
191
|
+
}
|
|
192
|
+
this.transitionState(message.delegationId, { type: 'RECEIVE_ACCEPT', message });
|
|
193
|
+
delegation.executorWorkDir = message.executorWorkDir;
|
|
194
|
+
delegation.executorConstraints = message.executorConstraints;
|
|
195
|
+
delegation.executorRetentionMs = message.retentionMs;
|
|
196
|
+
let stream;
|
|
197
|
+
try {
|
|
198
|
+
const handle = await this.transport.prepare({
|
|
199
|
+
delegationId: delegation.id,
|
|
200
|
+
exportPath: delegation.exportPath,
|
|
201
|
+
ttlSeconds: delegation.leaseConfig.ttlSeconds,
|
|
202
|
+
});
|
|
203
|
+
const expiresAt = new Date(Date.now() + delegation.leaseConfig.ttlSeconds * 1000).toISOString();
|
|
204
|
+
const startMessage = {
|
|
205
|
+
version: PROTOCOL_VERSION,
|
|
206
|
+
type: 'START',
|
|
207
|
+
delegationId: delegation.id,
|
|
208
|
+
lease: {
|
|
209
|
+
expiresAt,
|
|
210
|
+
accessMode: delegation.leaseConfig.accessMode,
|
|
211
|
+
},
|
|
212
|
+
transportHandle: handle,
|
|
213
|
+
};
|
|
214
|
+
stream = await this.executorClient.connectTaskEvents(delegation.peerUrl, delegation.id);
|
|
215
|
+
this.transitionState(delegation.id, { type: 'SEND_START', message: startMessage });
|
|
216
|
+
delegation.activeLease = startMessage.lease;
|
|
217
|
+
await this.executorClient.sendStart(delegation.peerUrl, startMessage);
|
|
116
218
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return;
|
|
219
|
+
catch (error) {
|
|
220
|
+
stream?.abort();
|
|
221
|
+
await this.transport.detach(delegation.id).catch(() => { });
|
|
222
|
+
throw error;
|
|
122
223
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
exportPath: updated.exportPath,
|
|
128
|
-
ttlSeconds: delegation.leaseConfig.ttlSeconds,
|
|
224
|
+
this.config.hooks.onDelegationStarted?.(delegation);
|
|
225
|
+
console.log(`[AWCP:Delegator] START sent for ${delegation.id}, consuming SSE events...`);
|
|
226
|
+
this.consumeTaskEvents(delegation.id, stream).catch((error) => {
|
|
227
|
+
console.error(`[AWCP:Delegator] SSE error for ${delegation.id}:`, error);
|
|
129
228
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
229
|
+
}
|
|
230
|
+
async handleDone(message) {
|
|
231
|
+
const delegation = this.delegations.get(message.delegationId);
|
|
232
|
+
if (!delegation) {
|
|
233
|
+
throw new Error(`Unknown delegation for DONE: ${message.delegationId}` +
|
|
234
|
+
` (known=[${Array.from(this.delegations.keys()).join(',')}])`);
|
|
235
|
+
}
|
|
236
|
+
const stateMachine = this.stateMachines.get(message.delegationId);
|
|
237
|
+
if (stateMachine.getState() === 'started') {
|
|
238
|
+
this.transitionState(message.delegationId, { type: 'SETUP_COMPLETE' });
|
|
239
|
+
}
|
|
240
|
+
this.transitionState(message.delegationId, { type: 'RECEIVE_DONE', message });
|
|
241
|
+
delegation.result = {
|
|
242
|
+
summary: message.finalSummary,
|
|
243
|
+
highlights: message.highlights,
|
|
140
244
|
};
|
|
141
|
-
this.
|
|
142
|
-
|
|
143
|
-
this.
|
|
144
|
-
|
|
145
|
-
this.config.hooks.onDelegationStarted?.(updated);
|
|
146
|
-
this.subscribeToTaskEvents(delegation.id, executorUrl);
|
|
245
|
+
await this.persistDelegation(delegation.id);
|
|
246
|
+
await this.transport.detach(delegation.id).catch(() => { });
|
|
247
|
+
await this.environmentManager.release(delegation.id);
|
|
248
|
+
this.config.hooks.onDelegationCompleted?.(delegation);
|
|
147
249
|
}
|
|
148
|
-
async
|
|
250
|
+
async handleError(message) {
|
|
251
|
+
const delegation = this.delegations.get(message.delegationId);
|
|
252
|
+
if (!delegation) {
|
|
253
|
+
throw new Error(`Unknown delegation for ERROR: ${message.delegationId}` +
|
|
254
|
+
` (known=[${Array.from(this.delegations.keys()).join(',')}])`);
|
|
255
|
+
}
|
|
256
|
+
console.log(`[AWCP:Delegator] Processing error for ${message.delegationId}` +
|
|
257
|
+
` (state=${delegation.state}): ${message.code} - ${message.message}`);
|
|
258
|
+
this.transitionState(message.delegationId, { type: 'RECEIVE_ERROR', message });
|
|
259
|
+
delegation.error = {
|
|
260
|
+
code: message.code,
|
|
261
|
+
message: message.message,
|
|
262
|
+
hint: message.hint,
|
|
263
|
+
};
|
|
264
|
+
await this.persistDelegation(delegation.id);
|
|
265
|
+
const error = new AwcpError(message.code, message.message, message.hint, delegation.id);
|
|
266
|
+
this.config.hooks.onError?.(delegation.id, error);
|
|
267
|
+
}
|
|
268
|
+
async consumeTaskEvents(delegationId, stream) {
|
|
149
269
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
console.log(`[AWCP:Delegator] SSE event for ${delegationId}: ${event.type}`);
|
|
270
|
+
for await (const event of stream.events) {
|
|
271
|
+
console.log(`[AWCP:Delegator] SSE event for ${delegationId}: type=${event.type}`);
|
|
153
272
|
await this.handleTaskEvent(delegationId, event);
|
|
154
273
|
if (event.type === 'done' || event.type === 'error') {
|
|
274
|
+
console.log(`[AWCP:Delegator] SSE stream ended for ${delegationId} (terminal event: ${event.type})`);
|
|
155
275
|
break;
|
|
156
276
|
}
|
|
157
277
|
}
|
|
158
278
|
}
|
|
159
279
|
catch (error) {
|
|
160
|
-
console.error(`[AWCP:Delegator] SSE
|
|
161
|
-
const
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
280
|
+
console.error(`[AWCP:Delegator] SSE connection lost for ${delegationId}:`, error instanceof Error ? error.message : error);
|
|
281
|
+
const current = this.delegations.get(delegationId);
|
|
282
|
+
if (current && !isTerminalState(current.state)) {
|
|
283
|
+
console.error(`[AWCP:Delegator] Marking ${delegationId} as error (was ${current.state})`);
|
|
284
|
+
current.state = 'error';
|
|
285
|
+
current.error = {
|
|
165
286
|
code: 'SSE_FAILED',
|
|
166
|
-
message: error instanceof Error ? error.message : '
|
|
287
|
+
message: `SSE connection lost: ${error instanceof Error ? error.message : 'unknown error'}`,
|
|
167
288
|
};
|
|
168
|
-
|
|
289
|
+
current.updatedAt = new Date().toISOString();
|
|
290
|
+
await this.persistDelegation(delegationId);
|
|
169
291
|
}
|
|
170
292
|
}
|
|
171
293
|
}
|
|
@@ -181,10 +303,7 @@ export class DelegatorService {
|
|
|
181
303
|
await this.handleSnapshotEvent(delegationId, event);
|
|
182
304
|
}
|
|
183
305
|
if (event.type === 'done') {
|
|
184
|
-
|
|
185
|
-
if (executorUrl) {
|
|
186
|
-
await this.executorClient.acknowledgeResult(executorUrl, delegationId).catch(() => { });
|
|
187
|
-
}
|
|
306
|
+
await this.executorClient.acknowledgeResult(delegation.peerUrl, delegationId).catch(() => { });
|
|
188
307
|
const doneMessage = {
|
|
189
308
|
version: PROTOCOL_VERSION,
|
|
190
309
|
type: 'DONE',
|
|
@@ -195,6 +314,8 @@ export class DelegatorService {
|
|
|
195
314
|
await this.handleDone(doneMessage);
|
|
196
315
|
}
|
|
197
316
|
if (event.type === 'error') {
|
|
317
|
+
console.error(`[AWCP:Delegator] Executor reported error for ${delegationId}:` +
|
|
318
|
+
` code=${event.code}, message=${event.message}`);
|
|
198
319
|
const errorMessage = {
|
|
199
320
|
version: PROTOCOL_VERSION,
|
|
200
321
|
type: 'ERROR',
|
|
@@ -207,137 +328,24 @@ export class DelegatorService {
|
|
|
207
328
|
}
|
|
208
329
|
}
|
|
209
330
|
async handleSnapshotEvent(delegationId, event) {
|
|
210
|
-
// liveSync transports don't produce snapshots
|
|
211
|
-
if (this.transport.capabilities.liveSync)
|
|
212
|
-
return;
|
|
213
|
-
const delegation = this.delegations.get(delegationId);
|
|
214
|
-
if (!delegation)
|
|
215
|
-
return;
|
|
216
|
-
const policy = delegation.snapshotPolicy ?? {
|
|
217
|
-
mode: this.config.snapshot.mode,
|
|
218
|
-
retentionMs: this.config.snapshot.retentionMs,
|
|
219
|
-
maxSnapshots: this.config.snapshot.maxSnapshots,
|
|
220
|
-
};
|
|
221
|
-
if (!delegation.snapshots) {
|
|
222
|
-
delegation.snapshots = [];
|
|
223
|
-
}
|
|
224
|
-
const snapshot = {
|
|
225
|
-
id: event.snapshotId,
|
|
226
|
-
delegationId,
|
|
227
|
-
summary: event.summary,
|
|
228
|
-
highlights: event.highlights,
|
|
229
|
-
status: 'pending',
|
|
230
|
-
metadata: event.metadata,
|
|
231
|
-
recommended: event.recommended,
|
|
232
|
-
createdAt: new Date().toISOString(),
|
|
233
|
-
};
|
|
234
|
-
if (policy.mode === 'auto') {
|
|
235
|
-
await this.applySnapshotToWorkspace(delegationId, event.snapshotBase64);
|
|
236
|
-
snapshot.status = 'applied';
|
|
237
|
-
snapshot.appliedAt = new Date().toISOString();
|
|
238
|
-
delegation.appliedSnapshotId = event.snapshotId;
|
|
239
|
-
}
|
|
240
|
-
else if (policy.mode === 'staged') {
|
|
241
|
-
const localPath = await this.snapshotStore.save(delegationId, event.snapshotId, event.snapshotBase64, { summary: event.summary, highlights: event.highlights, ...event.metadata });
|
|
242
|
-
snapshot.localPath = localPath;
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
snapshot.status = 'discarded';
|
|
246
|
-
}
|
|
247
|
-
delegation.snapshots.push(snapshot);
|
|
248
|
-
delegation.updatedAt = new Date().toISOString();
|
|
249
|
-
this.config.hooks.onSnapshotReceived?.(delegation, snapshot);
|
|
250
|
-
}
|
|
251
|
-
async applySnapshotToWorkspace(delegationId, snapshotData) {
|
|
252
331
|
const delegation = this.delegations.get(delegationId);
|
|
253
332
|
if (!delegation)
|
|
254
333
|
return;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const rwResources = delegation.environment.resources.filter(r => r.mode === 'rw');
|
|
258
|
-
if (rwResources.length === 0)
|
|
259
|
-
return;
|
|
260
|
-
try {
|
|
261
|
-
if (this.transport.applySnapshot) {
|
|
262
|
-
await this.transport.applySnapshot({
|
|
263
|
-
delegationId,
|
|
264
|
-
snapshotData,
|
|
265
|
-
resources: rwResources.map(r => ({ name: r.name, source: r.source, mode: r.mode })),
|
|
266
|
-
});
|
|
267
|
-
console.log(`[AWCP:Delegator] Applied snapshot for ${delegationId}`);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
catch (error) {
|
|
271
|
-
console.error(`[AWCP:Delegator] Failed to apply snapshot for ${delegationId}:`, error);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
async handleDone(message) {
|
|
275
|
-
const delegation = this.delegations.get(message.delegationId);
|
|
276
|
-
if (!delegation) {
|
|
277
|
-
console.warn(`[AWCP:Delegator] Unknown delegation for DONE: ${message.delegationId}`);
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
const stateMachine = this.stateMachines.get(message.delegationId);
|
|
281
|
-
if (stateMachine.getState() === 'started') {
|
|
282
|
-
this.transitionState(message.delegationId, { type: 'SETUP_COMPLETE' });
|
|
283
|
-
}
|
|
284
|
-
const result = this.transitionState(message.delegationId, { type: 'RECEIVE_DONE', message });
|
|
285
|
-
if (!result.success) {
|
|
286
|
-
console.error(`[AWCP:Delegator] State transition failed: ${result.error}`);
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
const updated = applyMessageToDelegation(delegation, message);
|
|
290
|
-
this.delegations.set(delegation.id, updated);
|
|
291
|
-
// liveSync transports: cleanup immediately (changes already synced)
|
|
292
|
-
// snapshot transports: cleanup based on policy
|
|
293
|
-
const shouldCleanupNow = this.transport.capabilities.liveSync
|
|
294
|
-
|| this.config.snapshot.mode === 'auto'
|
|
295
|
-
|| this.shouldCleanup(delegation);
|
|
296
|
-
if (shouldCleanupNow) {
|
|
297
|
-
await this.cleanup(delegation.id);
|
|
298
|
-
}
|
|
299
|
-
this.config.hooks.onDelegationCompleted?.(updated);
|
|
300
|
-
}
|
|
301
|
-
async handleError(message) {
|
|
302
|
-
const delegation = this.delegations.get(message.delegationId);
|
|
303
|
-
if (!delegation) {
|
|
304
|
-
console.warn(`[AWCP:Delegator] Unknown delegation for ERROR: ${message.delegationId}`);
|
|
334
|
+
const snapshot = await this.snapshotManager.receive(delegation, event);
|
|
335
|
+
if (!snapshot)
|
|
305
336
|
return;
|
|
306
|
-
|
|
307
|
-
this.
|
|
308
|
-
const updated = applyMessageToDelegation(delegation, message);
|
|
309
|
-
this.delegations.set(delegation.id, updated);
|
|
310
|
-
await this.cleanup(delegation.id);
|
|
311
|
-
const error = new AwcpError(message.code, message.message, message.hint, delegation.id);
|
|
312
|
-
this.config.hooks.onError?.(delegation.id, error);
|
|
313
|
-
}
|
|
314
|
-
async handleMessage(message) {
|
|
315
|
-
switch (message.type) {
|
|
316
|
-
case 'ACCEPT':
|
|
317
|
-
await this.handleAccept(message);
|
|
318
|
-
break;
|
|
319
|
-
case 'DONE':
|
|
320
|
-
await this.handleDone(message);
|
|
321
|
-
break;
|
|
322
|
-
case 'ERROR':
|
|
323
|
-
await this.handleError(message);
|
|
324
|
-
break;
|
|
325
|
-
default:
|
|
326
|
-
console.warn(`[AWCP:Delegator] Unexpected message type: ${message.type}`);
|
|
327
|
-
}
|
|
337
|
+
await this.persistDelegation(delegationId);
|
|
338
|
+
this.config.hooks.onSnapshotReceived?.(delegation, snapshot);
|
|
328
339
|
}
|
|
329
340
|
async cancel(delegationId) {
|
|
330
341
|
const delegation = this.delegations.get(delegationId);
|
|
331
342
|
if (!delegation) {
|
|
332
343
|
throw new Error(`Unknown delegation: ${delegationId}`);
|
|
333
344
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
await this.executorClient.sendCancel(executorUrl, delegationId).catch(console.error);
|
|
340
|
-
await this.cleanup(delegationId);
|
|
345
|
+
this.transitionState(delegationId, { type: 'CANCEL' });
|
|
346
|
+
await this.persistDelegation(delegationId);
|
|
347
|
+
await this.executorClient.sendCancel(delegation.peerUrl, delegationId).catch(console.error);
|
|
348
|
+
await this.transport.detach(delegationId).catch(() => { });
|
|
341
349
|
}
|
|
342
350
|
getDelegation(delegationId) {
|
|
343
351
|
return this.delegations.get(delegationId);
|
|
@@ -352,55 +360,16 @@ export class DelegatorService {
|
|
|
352
360
|
const delegation = this.delegations.get(delegationId);
|
|
353
361
|
if (!delegation)
|
|
354
362
|
throw new Error(`Unknown delegation: ${delegationId}`);
|
|
355
|
-
const snapshot =
|
|
356
|
-
|
|
357
|
-
throw new Error(`Snapshot not found: ${snapshotId}`);
|
|
358
|
-
if (snapshot.status === 'applied')
|
|
359
|
-
throw new Error(`Snapshot already applied: ${snapshotId}`);
|
|
360
|
-
const snapshotBuffer = await this.snapshotStore.load(delegationId, snapshotId);
|
|
361
|
-
const snapshotBase64 = snapshotBuffer.toString('base64');
|
|
362
|
-
await this.applySnapshotToWorkspace(delegationId, snapshotBase64);
|
|
363
|
-
snapshot.status = 'applied';
|
|
364
|
-
snapshot.appliedAt = new Date().toISOString();
|
|
365
|
-
delegation.appliedSnapshotId = snapshotId;
|
|
366
|
-
delegation.updatedAt = new Date().toISOString();
|
|
363
|
+
const snapshot = await this.snapshotManager.apply(delegation, snapshotId);
|
|
364
|
+
await this.persistDelegation(delegationId);
|
|
367
365
|
this.config.hooks.onSnapshotApplied?.(delegation, snapshot);
|
|
368
|
-
if (this.shouldCleanup(delegation)) {
|
|
369
|
-
await this.cleanup(delegationId);
|
|
370
|
-
}
|
|
371
366
|
}
|
|
372
367
|
async discardSnapshot(delegationId, snapshotId) {
|
|
373
368
|
const delegation = this.delegations.get(delegationId);
|
|
374
369
|
if (!delegation)
|
|
375
370
|
throw new Error(`Unknown delegation: ${delegationId}`);
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
throw new Error(`Snapshot not found: ${snapshotId}`);
|
|
379
|
-
await this.snapshotStore.delete(delegationId, snapshotId);
|
|
380
|
-
snapshot.status = 'discarded';
|
|
381
|
-
snapshot.localPath = undefined;
|
|
382
|
-
delegation.updatedAt = new Date().toISOString();
|
|
383
|
-
if (this.shouldCleanup(delegation)) {
|
|
384
|
-
await this.cleanup(delegationId);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
async waitForCompletion(delegationId, timeoutMs = 60000) {
|
|
388
|
-
const startTime = Date.now();
|
|
389
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
390
|
-
const delegation = this.delegations.get(delegationId);
|
|
391
|
-
if (!delegation) {
|
|
392
|
-
throw new Error(`Unknown delegation: ${delegationId}`);
|
|
393
|
-
}
|
|
394
|
-
const stateMachine = this.stateMachines.get(delegationId);
|
|
395
|
-
if (stateMachine.isTerminal()) {
|
|
396
|
-
if (delegation.error) {
|
|
397
|
-
throw new AwcpError(delegation.error.code, delegation.error.message, delegation.error.hint, delegationId);
|
|
398
|
-
}
|
|
399
|
-
return delegation;
|
|
400
|
-
}
|
|
401
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
402
|
-
}
|
|
403
|
-
throw new Error('Timeout waiting for delegation to complete');
|
|
371
|
+
await this.snapshotManager.discard(delegation, snapshotId);
|
|
372
|
+
await this.persistDelegation(delegationId);
|
|
404
373
|
}
|
|
405
374
|
getStatus() {
|
|
406
375
|
return {
|
|
@@ -414,34 +383,26 @@ export class DelegatorService {
|
|
|
414
383
|
})),
|
|
415
384
|
};
|
|
416
385
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
this.
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
shouldCleanup(delegation) {
|
|
424
|
-
if (!delegation.snapshots || delegation.snapshots.length === 0) {
|
|
425
|
-
return true;
|
|
386
|
+
async persistDelegation(delegationId) {
|
|
387
|
+
const delegation = this.delegations.get(delegationId);
|
|
388
|
+
if (delegation) {
|
|
389
|
+
await this.delegationManager.save(delegation);
|
|
426
390
|
}
|
|
427
|
-
return delegation.snapshots.every(s => s.status !== 'pending');
|
|
428
|
-
}
|
|
429
|
-
async cleanup(delegationId) {
|
|
430
|
-
await this.transport.cleanup(delegationId);
|
|
431
|
-
await this.environmentBuilder.release(delegationId);
|
|
432
|
-
await this.snapshotStore.cleanupDelegation(delegationId);
|
|
433
|
-
this.executorUrls.delete(delegationId);
|
|
434
391
|
}
|
|
435
392
|
startCleanupTimer() {
|
|
436
393
|
this.cleanupTimer = setInterval(async () => {
|
|
437
394
|
const now = Date.now();
|
|
438
395
|
for (const [id, delegation] of this.delegations) {
|
|
439
|
-
if (!
|
|
396
|
+
if (!isTerminalState(delegation.state))
|
|
440
397
|
continue;
|
|
441
|
-
const policy = delegation.snapshotPolicy ?? { retentionMs: this.config.snapshot.retentionMs };
|
|
442
398
|
const updatedAt = new Date(delegation.updatedAt).getTime();
|
|
443
|
-
if (now - updatedAt >
|
|
444
|
-
await this.
|
|
399
|
+
if (now - updatedAt > delegation.retentionMs) {
|
|
400
|
+
await this.transport.release(id).catch(() => { });
|
|
401
|
+
await this.environmentManager.release(id);
|
|
402
|
+
await this.snapshotManager.cleanupDelegation(id);
|
|
403
|
+
await this.delegationManager.delete(id).catch(() => { });
|
|
404
|
+
this.delegations.delete(id);
|
|
405
|
+
this.stateMachines.delete(id);
|
|
445
406
|
}
|
|
446
407
|
}
|
|
447
408
|
}, 60 * 1000);
|
|
@@ -450,11 +411,11 @@ export class DelegatorService {
|
|
|
450
411
|
const sm = this.stateMachines.get(delegationId);
|
|
451
412
|
const delegation = this.delegations.get(delegationId);
|
|
452
413
|
const result = sm.transition(event);
|
|
453
|
-
if (result.success) {
|
|
454
|
-
delegation.state
|
|
455
|
-
delegation.updatedAt = new Date().toISOString();
|
|
414
|
+
if (!result.success) {
|
|
415
|
+
throw new Error(`Cannot transition delegation ${delegationId} (${event.type}) in state '${delegation.state}': ${result.error}`);
|
|
456
416
|
}
|
|
457
|
-
|
|
417
|
+
delegation.state = sm.getState();
|
|
418
|
+
delegation.updatedAt = new Date().toISOString();
|
|
458
419
|
}
|
|
459
420
|
async validateAndNormalizePath(localDir, delegationId) {
|
|
460
421
|
const absolutePath = path.isAbsolute(localDir)
|