@awcp/sdk 0.0.17 → 0.0.19
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/daemon.d.ts +1 -1
- package/dist/delegator/bin/daemon.d.ts.map +1 -1
- package/dist/delegator/bin/daemon.js +5 -3
- 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 +27 -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 +253 -300
- 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 +320 -234
- 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 +7 -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 +4 -3
- 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,151 +5,269 @@
|
|
|
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(',')}])`);
|
|
116
191
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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);
|
|
122
218
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
219
|
+
catch (error) {
|
|
220
|
+
stream?.abort();
|
|
221
|
+
await this.transport.detach(delegation.id).catch(() => { });
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
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,
|
|
244
|
+
};
|
|
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);
|
|
249
|
+
}
|
|
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,
|
|
140
263
|
};
|
|
141
|
-
this.
|
|
142
|
-
|
|
143
|
-
this.
|
|
144
|
-
await this.executorClient.sendStart(executorUrl, startMessage);
|
|
145
|
-
this.config.hooks.onDelegationStarted?.(updated);
|
|
146
|
-
console.log(`[AWCP:Delegator] START sent for ${delegation.id}, subscribing to SSE...`);
|
|
147
|
-
this.subscribeToTaskEvents(delegation.id, executorUrl);
|
|
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);
|
|
148
267
|
}
|
|
149
|
-
async
|
|
268
|
+
async consumeTaskEvents(delegationId, stream) {
|
|
150
269
|
try {
|
|
151
|
-
|
|
152
|
-
for await (const event of this.executorClient.subscribeTask(executorUrl, delegationId)) {
|
|
270
|
+
for await (const event of stream.events) {
|
|
153
271
|
console.log(`[AWCP:Delegator] SSE event for ${delegationId}: type=${event.type}`);
|
|
154
272
|
await this.handleTaskEvent(delegationId, event);
|
|
155
273
|
if (event.type === 'done' || event.type === 'error') {
|
|
@@ -160,15 +278,16 @@ export class DelegatorService {
|
|
|
160
278
|
}
|
|
161
279
|
catch (error) {
|
|
162
280
|
console.error(`[AWCP:Delegator] SSE connection lost for ${delegationId}:`, error instanceof Error ? error.message : error);
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
165
|
-
console.error(`[AWCP:Delegator] Marking ${delegationId} as error (was ${
|
|
166
|
-
|
|
167
|
-
|
|
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 = {
|
|
168
286
|
code: 'SSE_FAILED',
|
|
169
|
-
message: `SSE connection
|
|
287
|
+
message: `SSE connection lost: ${error instanceof Error ? error.message : 'unknown error'}`,
|
|
170
288
|
};
|
|
171
|
-
|
|
289
|
+
current.updatedAt = new Date().toISOString();
|
|
290
|
+
await this.persistDelegation(delegationId);
|
|
172
291
|
}
|
|
173
292
|
}
|
|
174
293
|
}
|
|
@@ -184,10 +303,7 @@ export class DelegatorService {
|
|
|
184
303
|
await this.handleSnapshotEvent(delegationId, event);
|
|
185
304
|
}
|
|
186
305
|
if (event.type === 'done') {
|
|
187
|
-
|
|
188
|
-
if (executorUrl) {
|
|
189
|
-
await this.executorClient.acknowledgeResult(executorUrl, delegationId).catch(() => { });
|
|
190
|
-
}
|
|
306
|
+
await this.executorClient.acknowledgeResult(delegation.peerUrl, delegationId).catch(() => { });
|
|
191
307
|
const doneMessage = {
|
|
192
308
|
version: PROTOCOL_VERSION,
|
|
193
309
|
type: 'DONE',
|
|
@@ -212,140 +328,24 @@ export class DelegatorService {
|
|
|
212
328
|
}
|
|
213
329
|
}
|
|
214
330
|
async handleSnapshotEvent(delegationId, event) {
|
|
215
|
-
// liveSync transports don't produce snapshots
|
|
216
|
-
if (this.transport.capabilities.liveSync)
|
|
217
|
-
return;
|
|
218
331
|
const delegation = this.delegations.get(delegationId);
|
|
219
332
|
if (!delegation)
|
|
220
333
|
return;
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
retentionMs: this.config.snapshot.retentionMs,
|
|
224
|
-
maxSnapshots: this.config.snapshot.maxSnapshots,
|
|
225
|
-
};
|
|
226
|
-
if (!delegation.snapshots) {
|
|
227
|
-
delegation.snapshots = [];
|
|
228
|
-
}
|
|
229
|
-
const snapshot = {
|
|
230
|
-
id: event.snapshotId,
|
|
231
|
-
delegationId,
|
|
232
|
-
summary: event.summary,
|
|
233
|
-
highlights: event.highlights,
|
|
234
|
-
status: 'pending',
|
|
235
|
-
metadata: event.metadata,
|
|
236
|
-
recommended: event.recommended,
|
|
237
|
-
createdAt: new Date().toISOString(),
|
|
238
|
-
};
|
|
239
|
-
if (policy.mode === 'auto') {
|
|
240
|
-
await this.applySnapshotToWorkspace(delegationId, event.snapshotBase64);
|
|
241
|
-
snapshot.status = 'applied';
|
|
242
|
-
snapshot.appliedAt = new Date().toISOString();
|
|
243
|
-
delegation.appliedSnapshotId = event.snapshotId;
|
|
244
|
-
}
|
|
245
|
-
else if (policy.mode === 'staged') {
|
|
246
|
-
const localPath = await this.snapshotStore.save(delegationId, event.snapshotId, event.snapshotBase64, { summary: event.summary, highlights: event.highlights, ...event.metadata });
|
|
247
|
-
snapshot.localPath = localPath;
|
|
248
|
-
}
|
|
249
|
-
else {
|
|
250
|
-
snapshot.status = 'discarded';
|
|
251
|
-
}
|
|
252
|
-
delegation.snapshots.push(snapshot);
|
|
253
|
-
delegation.updatedAt = new Date().toISOString();
|
|
254
|
-
this.config.hooks.onSnapshotReceived?.(delegation, snapshot);
|
|
255
|
-
}
|
|
256
|
-
async applySnapshotToWorkspace(delegationId, snapshotData) {
|
|
257
|
-
const delegation = this.delegations.get(delegationId);
|
|
258
|
-
if (!delegation)
|
|
259
|
-
return;
|
|
260
|
-
if (!this.environmentBuilder.get(delegationId))
|
|
261
|
-
return;
|
|
262
|
-
const rwResources = delegation.environment.resources.filter(r => r.mode === 'rw');
|
|
263
|
-
if (rwResources.length === 0)
|
|
264
|
-
return;
|
|
265
|
-
try {
|
|
266
|
-
if (this.transport.applySnapshot) {
|
|
267
|
-
await this.transport.applySnapshot({
|
|
268
|
-
delegationId,
|
|
269
|
-
snapshotData,
|
|
270
|
-
resources: rwResources.map(r => ({ name: r.name, source: r.source, mode: r.mode })),
|
|
271
|
-
});
|
|
272
|
-
console.log(`[AWCP:Delegator] Applied snapshot for ${delegationId}`);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
catch (error) {
|
|
276
|
-
console.error(`[AWCP:Delegator] Failed to apply snapshot for ${delegationId}:`, error);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
async handleDone(message) {
|
|
280
|
-
const delegation = this.delegations.get(message.delegationId);
|
|
281
|
-
if (!delegation) {
|
|
282
|
-
console.warn(`[AWCP:Delegator] Unknown delegation for DONE: ${message.delegationId}`);
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
const stateMachine = this.stateMachines.get(message.delegationId);
|
|
286
|
-
if (stateMachine.getState() === 'started') {
|
|
287
|
-
this.transitionState(message.delegationId, { type: 'SETUP_COMPLETE' });
|
|
288
|
-
}
|
|
289
|
-
const result = this.transitionState(message.delegationId, { type: 'RECEIVE_DONE', message });
|
|
290
|
-
if (!result.success) {
|
|
291
|
-
console.error(`[AWCP:Delegator] State transition failed: ${result.error}`);
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
const updated = applyMessageToDelegation(delegation, message);
|
|
295
|
-
this.delegations.set(delegation.id, updated);
|
|
296
|
-
// liveSync transports: cleanup immediately (changes already synced)
|
|
297
|
-
// snapshot transports: cleanup based on policy
|
|
298
|
-
const shouldCleanupNow = this.transport.capabilities.liveSync
|
|
299
|
-
|| this.config.snapshot.mode === 'auto'
|
|
300
|
-
|| this.shouldCleanup(delegation);
|
|
301
|
-
if (shouldCleanupNow) {
|
|
302
|
-
await this.cleanup(delegation.id);
|
|
303
|
-
}
|
|
304
|
-
this.config.hooks.onDelegationCompleted?.(updated);
|
|
305
|
-
}
|
|
306
|
-
async handleError(message) {
|
|
307
|
-
const delegation = this.delegations.get(message.delegationId);
|
|
308
|
-
if (!delegation) {
|
|
309
|
-
console.warn(`[AWCP:Delegator] Received ERROR for unknown delegation ${message.delegationId}` +
|
|
310
|
-
` (known=[${Array.from(this.delegations.keys()).join(',')}])`);
|
|
334
|
+
const snapshot = await this.snapshotManager.receive(delegation, event);
|
|
335
|
+
if (!snapshot)
|
|
311
336
|
return;
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
` (state=${delegation.state}): ${message.code} - ${message.message}`);
|
|
315
|
-
this.transitionState(message.delegationId, { type: 'RECEIVE_ERROR', message });
|
|
316
|
-
const updated = applyMessageToDelegation(delegation, message);
|
|
317
|
-
this.delegations.set(delegation.id, updated);
|
|
318
|
-
await this.cleanup(delegation.id);
|
|
319
|
-
const error = new AwcpError(message.code, message.message, message.hint, delegation.id);
|
|
320
|
-
this.config.hooks.onError?.(delegation.id, error);
|
|
321
|
-
}
|
|
322
|
-
async handleMessage(message) {
|
|
323
|
-
switch (message.type) {
|
|
324
|
-
case 'ACCEPT':
|
|
325
|
-
await this.handleAccept(message);
|
|
326
|
-
break;
|
|
327
|
-
case 'DONE':
|
|
328
|
-
await this.handleDone(message);
|
|
329
|
-
break;
|
|
330
|
-
case 'ERROR':
|
|
331
|
-
await this.handleError(message);
|
|
332
|
-
break;
|
|
333
|
-
default:
|
|
334
|
-
console.warn(`[AWCP:Delegator] Unexpected message type: ${message.type}`);
|
|
335
|
-
}
|
|
337
|
+
await this.persistDelegation(delegationId);
|
|
338
|
+
this.config.hooks.onSnapshotReceived?.(delegation, snapshot);
|
|
336
339
|
}
|
|
337
340
|
async cancel(delegationId) {
|
|
338
341
|
const delegation = this.delegations.get(delegationId);
|
|
339
342
|
if (!delegation) {
|
|
340
343
|
throw new Error(`Unknown delegation: ${delegationId}`);
|
|
341
344
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
await this.executorClient.sendCancel(executorUrl, delegationId).catch(console.error);
|
|
348
|
-
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(() => { });
|
|
349
349
|
}
|
|
350
350
|
getDelegation(delegationId) {
|
|
351
351
|
return this.delegations.get(delegationId);
|
|
@@ -360,55 +360,16 @@ export class DelegatorService {
|
|
|
360
360
|
const delegation = this.delegations.get(delegationId);
|
|
361
361
|
if (!delegation)
|
|
362
362
|
throw new Error(`Unknown delegation: ${delegationId}`);
|
|
363
|
-
const snapshot =
|
|
364
|
-
|
|
365
|
-
throw new Error(`Snapshot not found: ${snapshotId}`);
|
|
366
|
-
if (snapshot.status === 'applied')
|
|
367
|
-
throw new Error(`Snapshot already applied: ${snapshotId}`);
|
|
368
|
-
const snapshotBuffer = await this.snapshotStore.load(delegationId, snapshotId);
|
|
369
|
-
const snapshotBase64 = snapshotBuffer.toString('base64');
|
|
370
|
-
await this.applySnapshotToWorkspace(delegationId, snapshotBase64);
|
|
371
|
-
snapshot.status = 'applied';
|
|
372
|
-
snapshot.appliedAt = new Date().toISOString();
|
|
373
|
-
delegation.appliedSnapshotId = snapshotId;
|
|
374
|
-
delegation.updatedAt = new Date().toISOString();
|
|
363
|
+
const snapshot = await this.snapshotManager.apply(delegation, snapshotId);
|
|
364
|
+
await this.persistDelegation(delegationId);
|
|
375
365
|
this.config.hooks.onSnapshotApplied?.(delegation, snapshot);
|
|
376
|
-
if (this.shouldCleanup(delegation)) {
|
|
377
|
-
await this.cleanup(delegationId);
|
|
378
|
-
}
|
|
379
366
|
}
|
|
380
367
|
async discardSnapshot(delegationId, snapshotId) {
|
|
381
368
|
const delegation = this.delegations.get(delegationId);
|
|
382
369
|
if (!delegation)
|
|
383
370
|
throw new Error(`Unknown delegation: ${delegationId}`);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
throw new Error(`Snapshot not found: ${snapshotId}`);
|
|
387
|
-
await this.snapshotStore.delete(delegationId, snapshotId);
|
|
388
|
-
snapshot.status = 'discarded';
|
|
389
|
-
snapshot.localPath = undefined;
|
|
390
|
-
delegation.updatedAt = new Date().toISOString();
|
|
391
|
-
if (this.shouldCleanup(delegation)) {
|
|
392
|
-
await this.cleanup(delegationId);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
async waitForCompletion(delegationId, timeoutMs = 60000) {
|
|
396
|
-
const startTime = Date.now();
|
|
397
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
398
|
-
const delegation = this.delegations.get(delegationId);
|
|
399
|
-
if (!delegation) {
|
|
400
|
-
throw new Error(`Unknown delegation: ${delegationId}`);
|
|
401
|
-
}
|
|
402
|
-
const stateMachine = this.stateMachines.get(delegationId);
|
|
403
|
-
if (stateMachine.isTerminal()) {
|
|
404
|
-
if (delegation.error) {
|
|
405
|
-
throw new AwcpError(delegation.error.code, delegation.error.message, delegation.error.hint, delegationId);
|
|
406
|
-
}
|
|
407
|
-
return delegation;
|
|
408
|
-
}
|
|
409
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
410
|
-
}
|
|
411
|
-
throw new Error('Timeout waiting for delegation to complete');
|
|
371
|
+
await this.snapshotManager.discard(delegation, snapshotId);
|
|
372
|
+
await this.persistDelegation(delegationId);
|
|
412
373
|
}
|
|
413
374
|
getStatus() {
|
|
414
375
|
return {
|
|
@@ -422,34 +383,26 @@ export class DelegatorService {
|
|
|
422
383
|
})),
|
|
423
384
|
};
|
|
424
385
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
this.
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
shouldCleanup(delegation) {
|
|
432
|
-
if (!delegation.snapshots || delegation.snapshots.length === 0) {
|
|
433
|
-
return true;
|
|
386
|
+
async persistDelegation(delegationId) {
|
|
387
|
+
const delegation = this.delegations.get(delegationId);
|
|
388
|
+
if (delegation) {
|
|
389
|
+
await this.delegationManager.save(delegation);
|
|
434
390
|
}
|
|
435
|
-
return delegation.snapshots.every(s => s.status !== 'pending');
|
|
436
|
-
}
|
|
437
|
-
async cleanup(delegationId) {
|
|
438
|
-
await this.transport.cleanup(delegationId);
|
|
439
|
-
await this.environmentBuilder.release(delegationId);
|
|
440
|
-
await this.snapshotStore.cleanupDelegation(delegationId);
|
|
441
|
-
this.executorUrls.delete(delegationId);
|
|
442
391
|
}
|
|
443
392
|
startCleanupTimer() {
|
|
444
393
|
this.cleanupTimer = setInterval(async () => {
|
|
445
394
|
const now = Date.now();
|
|
446
395
|
for (const [id, delegation] of this.delegations) {
|
|
447
|
-
if (!
|
|
396
|
+
if (!isTerminalState(delegation.state))
|
|
448
397
|
continue;
|
|
449
|
-
const policy = delegation.snapshotPolicy ?? { retentionMs: this.config.snapshot.retentionMs };
|
|
450
398
|
const updatedAt = new Date(delegation.updatedAt).getTime();
|
|
451
|
-
if (now - updatedAt >
|
|
452
|
-
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);
|
|
453
406
|
}
|
|
454
407
|
}
|
|
455
408
|
}, 60 * 1000);
|
|
@@ -458,11 +411,11 @@ export class DelegatorService {
|
|
|
458
411
|
const sm = this.stateMachines.get(delegationId);
|
|
459
412
|
const delegation = this.delegations.get(delegationId);
|
|
460
413
|
const result = sm.transition(event);
|
|
461
|
-
if (result.success) {
|
|
462
|
-
delegation.state
|
|
463
|
-
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}`);
|
|
464
416
|
}
|
|
465
|
-
|
|
417
|
+
delegation.state = sm.getState();
|
|
418
|
+
delegation.updatedAt = new Date().toISOString();
|
|
466
419
|
}
|
|
467
420
|
async validateAndNormalizePath(localDir, delegationId) {
|
|
468
421
|
const absolutePath = path.isAbsolute(localDir)
|