@awcp/sdk 0.0.17 → 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.
Files changed (93) hide show
  1. package/dist/delegator/admission.d.ts +7 -28
  2. package/dist/delegator/admission.d.ts.map +1 -1
  3. package/dist/delegator/admission.js +35 -26
  4. package/dist/delegator/admission.js.map +1 -1
  5. package/dist/delegator/bin/client.d.ts +2 -2
  6. package/dist/delegator/bin/client.d.ts.map +1 -1
  7. package/dist/delegator/bin/daemon.d.ts +1 -1
  8. package/dist/delegator/bin/daemon.d.ts.map +1 -1
  9. package/dist/delegator/bin/daemon.js +5 -3
  10. package/dist/delegator/bin/daemon.js.map +1 -1
  11. package/dist/delegator/config.d.ts +57 -78
  12. package/dist/delegator/config.d.ts.map +1 -1
  13. package/dist/delegator/config.js +40 -24
  14. package/dist/delegator/config.js.map +1 -1
  15. package/dist/delegator/delegation-manager.d.ts +12 -0
  16. package/dist/delegator/delegation-manager.d.ts.map +1 -0
  17. package/dist/delegator/delegation-manager.js +36 -0
  18. package/dist/delegator/delegation-manager.js.map +1 -0
  19. package/dist/delegator/{environment-builder.d.ts → environment-manager.d.ts} +6 -12
  20. package/dist/delegator/{environment-builder.d.ts.map → environment-manager.d.ts.map} +1 -1
  21. package/dist/delegator/{environment-builder.js → environment-manager.js} +8 -26
  22. package/dist/delegator/environment-manager.js.map +1 -0
  23. package/dist/delegator/executor-client.d.ts +8 -25
  24. package/dist/delegator/executor-client.d.ts.map +1 -1
  25. package/dist/delegator/executor-client.js +27 -53
  26. package/dist/delegator/executor-client.js.map +1 -1
  27. package/dist/delegator/index.d.ts +5 -4
  28. package/dist/delegator/index.d.ts.map +1 -1
  29. package/dist/delegator/index.js +3 -2
  30. package/dist/delegator/index.js.map +1 -1
  31. package/dist/delegator/service.d.ts +28 -14
  32. package/dist/delegator/service.d.ts.map +1 -1
  33. package/dist/delegator/service.js +253 -300
  34. package/dist/delegator/service.js.map +1 -1
  35. package/dist/delegator/snapshot-manager.d.ts +23 -0
  36. package/dist/delegator/snapshot-manager.d.ts.map +1 -0
  37. package/dist/delegator/snapshot-manager.js +120 -0
  38. package/dist/delegator/snapshot-manager.js.map +1 -0
  39. package/dist/executor/a2a-adapter.d.ts +1 -1
  40. package/dist/executor/a2a-adapter.d.ts.map +1 -1
  41. package/dist/executor/admission.d.ts +13 -0
  42. package/dist/executor/admission.d.ts.map +1 -0
  43. package/dist/executor/admission.js +27 -0
  44. package/dist/executor/admission.js.map +1 -0
  45. package/dist/executor/assignment-manager.d.ts +12 -0
  46. package/dist/executor/assignment-manager.d.ts.map +1 -0
  47. package/dist/executor/assignment-manager.js +36 -0
  48. package/dist/executor/assignment-manager.js.map +1 -0
  49. package/dist/executor/config.d.ts +37 -24
  50. package/dist/executor/config.d.ts.map +1 -1
  51. package/dist/executor/config.js +21 -15
  52. package/dist/executor/config.js.map +1 -1
  53. package/dist/executor/index.d.ts +4 -2
  54. package/dist/executor/index.d.ts.map +1 -1
  55. package/dist/executor/index.js +3 -1
  56. package/dist/executor/index.js.map +1 -1
  57. package/dist/executor/service.d.ts +18 -12
  58. package/dist/executor/service.d.ts.map +1 -1
  59. package/dist/executor/service.js +320 -234
  60. package/dist/executor/service.js.map +1 -1
  61. package/dist/executor/workspace-manager.d.ts +1 -8
  62. package/dist/executor/workspace-manager.d.ts.map +1 -1
  63. package/dist/executor/workspace-manager.js +3 -25
  64. package/dist/executor/workspace-manager.js.map +1 -1
  65. package/dist/index.d.ts +3 -3
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +1 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/listener/http-listener.d.ts +1 -1
  70. package/dist/listener/http-listener.d.ts.map +1 -1
  71. package/dist/listener/http-listener.js +7 -1
  72. package/dist/listener/http-listener.js.map +1 -1
  73. package/dist/listener/index.d.ts +1 -0
  74. package/dist/listener/index.d.ts.map +1 -1
  75. package/dist/listener/index.js.map +1 -1
  76. package/dist/listener/types.d.ts +52 -0
  77. package/dist/listener/types.d.ts.map +1 -0
  78. package/dist/listener/types.js +5 -0
  79. package/dist/listener/types.js.map +1 -0
  80. package/dist/listener/websocket-tunnel-listener.d.ts +1 -1
  81. package/dist/listener/websocket-tunnel-listener.d.ts.map +1 -1
  82. package/dist/server/express/awcp-executor-handler.d.ts +3 -3
  83. package/dist/server/express/awcp-executor-handler.d.ts.map +1 -1
  84. package/dist/server/express/awcp-executor-handler.js +3 -1
  85. package/dist/server/express/awcp-executor-handler.js.map +1 -1
  86. package/dist/utils/fs-helpers.d.ts +1 -1
  87. package/dist/utils/fs-helpers.js +1 -1
  88. package/package.json +2 -2
  89. package/dist/delegator/environment-builder.js.map +0 -1
  90. package/dist/delegator/snapshot-store.d.ts +0 -27
  91. package/dist/delegator/snapshot-store.d.ts.map +0 -1
  92. package/dist/delegator/snapshot-store.js +0 -40
  93. 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, createDelegation, applyMessageToDelegation, generateDelegationId, PROTOCOL_VERSION, AwcpError, WorkspaceTooLargeError, WorkspaceNotFoundError, WorkspaceInvalidError, } from '@awcp/core';
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 { EnvironmentBuilder } from './environment-builder.js';
11
+ import { DelegationManager } from './delegation-manager.js';
12
+ import { EnvironmentManager } from './environment-manager.js';
12
13
  import { ExecutorClient } from './executor-client.js';
13
- import { SnapshotStore } from './snapshot-store.js';
14
+ import { SnapshotManager } from './snapshot-manager.js';
14
15
  export class DelegatorService {
15
16
  config;
16
17
  transport;
17
18
  admissionController;
18
- environmentBuilder;
19
- snapshotStore;
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
- // liveSync transports don't support staged snapshots
29
- if (this.transport.capabilities.liveSync && this.config.snapshot.mode === 'staged') {
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
- maxTotalBytes: this.config.admission.maxTotalBytes,
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.snapshotStore = new SnapshotStore({
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
- this.executorClient = new ExecutorClient();
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 delegationId = generateDelegationId();
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
- const admissionResult = await this.admissionController.check(sourcePath);
52
- if (!admissionResult.allowed) {
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.defaults.ttlSeconds;
57
- const accessMode = params.accessMode ?? this.config.defaults.accessMode;
58
- const snapshotMode = params.snapshotMode ?? this.config.snapshot.mode;
59
- const delegation = createDelegation({
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?.(delegation);
168
+ this.config.hooks.onDelegationCreated?.(this.delegations.get(delegationId));
104
169
  return delegationId;
105
170
  }
106
171
  catch (error) {
107
- await this.cleanup(delegationId);
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
- console.warn(`[AWCP:Delegator] Unknown delegation for ACCEPT: ${message.delegationId}`);
115
- return;
189
+ throw new Error(`Unknown delegation for ACCEPT: ${message.delegationId}` +
190
+ ` (known=[${Array.from(this.delegations.keys()).join(',')}])`);
116
191
  }
117
- const executorUrl = this.executorUrls.get(message.delegationId);
118
- const result = this.transitionState(message.delegationId, { type: 'RECEIVE_ACCEPT', message });
119
- if (!result.success) {
120
- console.error(`[AWCP:Delegator] State transition failed: ${result.error}`);
121
- return;
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
- const updated = applyMessageToDelegation(delegation, message);
124
- this.delegations.set(delegation.id, updated);
125
- const { workDirInfo } = await this.transport.prepare({
126
- delegationId: delegation.id,
127
- exportPath: updated.exportPath,
128
- ttlSeconds: delegation.leaseConfig.ttlSeconds,
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
- const expiresAt = new Date(Date.now() + delegation.leaseConfig.ttlSeconds * 1000).toISOString();
131
- const startMessage = {
132
- version: PROTOCOL_VERSION,
133
- type: 'START',
134
- delegationId: delegation.id,
135
- lease: {
136
- expiresAt,
137
- accessMode: delegation.leaseConfig.accessMode,
138
- },
139
- workDir: workDirInfo,
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.transitionState(delegation.id, { type: 'SEND_START', message: startMessage });
142
- updated.activeLease = startMessage.lease;
143
- this.delegations.set(delegation.id, updated);
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 subscribeToTaskEvents(delegationId, executorUrl) {
268
+ async consumeTaskEvents(delegationId, stream) {
150
269
  try {
151
- console.log(`[AWCP:Delegator] Opening SSE stream for ${delegationId} → ${executorUrl}`);
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 delegation = this.delegations.get(delegationId);
164
- if (delegation && !['completed', 'error', 'cancelled'].includes(delegation.state)) {
165
- console.error(`[AWCP:Delegator] Marking ${delegationId} as error (was ${delegation.state})`);
166
- delegation.state = 'error';
167
- delegation.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 = {
168
286
  code: 'SSE_FAILED',
169
- message: `SSE connection to executor lost: ${error instanceof Error ? error.message : 'unknown error'}`,
287
+ message: `SSE connection lost: ${error instanceof Error ? error.message : 'unknown error'}`,
170
288
  };
171
- this.delegations.set(delegationId, delegation);
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
- const executorUrl = this.executorUrls.get(delegationId);
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 policy = delegation.snapshotPolicy ?? {
222
- mode: this.config.snapshot.mode,
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
- console.log(`[AWCP:Delegator] Processing error for ${message.delegationId}` +
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
- const executorUrl = this.executorUrls.get(delegationId);
343
- const result = this.transitionState(delegationId, { type: 'CANCEL' });
344
- if (!result.success) {
345
- throw new Error(`Cannot cancel delegation in state ${delegation.state}`);
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 = delegation.snapshots?.find(s => s.id === snapshotId);
364
- if (!snapshot)
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
- const snapshot = delegation.snapshots?.find(s => s.id === snapshotId);
385
- if (!snapshot)
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
- stop() {
426
- if (this.cleanupTimer) {
427
- clearInterval(this.cleanupTimer);
428
- this.cleanupTimer = undefined;
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 (!['completed', 'error', 'cancelled'].includes(delegation.state))
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 > (policy.retentionMs ?? this.config.snapshot.retentionMs)) {
452
- await this.cleanup(id);
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 = sm.getState();
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
- return result;
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)