@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.
Files changed (95) 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/client.js +6 -2
  8. package/dist/delegator/bin/client.js.map +1 -1
  9. package/dist/delegator/bin/daemon.d.ts +1 -1
  10. package/dist/delegator/bin/daemon.d.ts.map +1 -1
  11. package/dist/delegator/bin/daemon.js +13 -4
  12. package/dist/delegator/bin/daemon.js.map +1 -1
  13. package/dist/delegator/config.d.ts +57 -78
  14. package/dist/delegator/config.d.ts.map +1 -1
  15. package/dist/delegator/config.js +40 -24
  16. package/dist/delegator/config.js.map +1 -1
  17. package/dist/delegator/delegation-manager.d.ts +12 -0
  18. package/dist/delegator/delegation-manager.d.ts.map +1 -0
  19. package/dist/delegator/delegation-manager.js +36 -0
  20. package/dist/delegator/delegation-manager.js.map +1 -0
  21. package/dist/delegator/{environment-builder.d.ts → environment-manager.d.ts} +6 -12
  22. package/dist/delegator/{environment-builder.d.ts.map → environment-manager.d.ts.map} +1 -1
  23. package/dist/delegator/{environment-builder.js → environment-manager.js} +8 -26
  24. package/dist/delegator/environment-manager.js.map +1 -0
  25. package/dist/delegator/executor-client.d.ts +8 -25
  26. package/dist/delegator/executor-client.d.ts.map +1 -1
  27. package/dist/delegator/executor-client.js +33 -53
  28. package/dist/delegator/executor-client.js.map +1 -1
  29. package/dist/delegator/index.d.ts +5 -4
  30. package/dist/delegator/index.d.ts.map +1 -1
  31. package/dist/delegator/index.js +3 -2
  32. package/dist/delegator/index.js.map +1 -1
  33. package/dist/delegator/service.d.ts +28 -14
  34. package/dist/delegator/service.d.ts.map +1 -1
  35. package/dist/delegator/service.js +258 -297
  36. package/dist/delegator/service.js.map +1 -1
  37. package/dist/delegator/snapshot-manager.d.ts +23 -0
  38. package/dist/delegator/snapshot-manager.d.ts.map +1 -0
  39. package/dist/delegator/snapshot-manager.js +120 -0
  40. package/dist/delegator/snapshot-manager.js.map +1 -0
  41. package/dist/executor/a2a-adapter.d.ts +1 -1
  42. package/dist/executor/a2a-adapter.d.ts.map +1 -1
  43. package/dist/executor/admission.d.ts +13 -0
  44. package/dist/executor/admission.d.ts.map +1 -0
  45. package/dist/executor/admission.js +27 -0
  46. package/dist/executor/admission.js.map +1 -0
  47. package/dist/executor/assignment-manager.d.ts +12 -0
  48. package/dist/executor/assignment-manager.d.ts.map +1 -0
  49. package/dist/executor/assignment-manager.js +36 -0
  50. package/dist/executor/assignment-manager.js.map +1 -0
  51. package/dist/executor/config.d.ts +37 -24
  52. package/dist/executor/config.d.ts.map +1 -1
  53. package/dist/executor/config.js +21 -15
  54. package/dist/executor/config.js.map +1 -1
  55. package/dist/executor/index.d.ts +4 -2
  56. package/dist/executor/index.d.ts.map +1 -1
  57. package/dist/executor/index.js +3 -1
  58. package/dist/executor/index.js.map +1 -1
  59. package/dist/executor/service.d.ts +18 -12
  60. package/dist/executor/service.d.ts.map +1 -1
  61. package/dist/executor/service.js +325 -211
  62. package/dist/executor/service.js.map +1 -1
  63. package/dist/executor/workspace-manager.d.ts +1 -8
  64. package/dist/executor/workspace-manager.d.ts.map +1 -1
  65. package/dist/executor/workspace-manager.js +3 -25
  66. package/dist/executor/workspace-manager.js.map +1 -1
  67. package/dist/index.d.ts +3 -3
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +1 -1
  70. package/dist/index.js.map +1 -1
  71. package/dist/listener/http-listener.d.ts +1 -1
  72. package/dist/listener/http-listener.d.ts.map +1 -1
  73. package/dist/listener/http-listener.js +8 -1
  74. package/dist/listener/http-listener.js.map +1 -1
  75. package/dist/listener/index.d.ts +1 -0
  76. package/dist/listener/index.d.ts.map +1 -1
  77. package/dist/listener/index.js.map +1 -1
  78. package/dist/listener/types.d.ts +52 -0
  79. package/dist/listener/types.d.ts.map +1 -0
  80. package/dist/listener/types.js +5 -0
  81. package/dist/listener/types.js.map +1 -0
  82. package/dist/listener/websocket-tunnel-listener.d.ts +1 -1
  83. package/dist/listener/websocket-tunnel-listener.d.ts.map +1 -1
  84. package/dist/server/express/awcp-executor-handler.d.ts +3 -3
  85. package/dist/server/express/awcp-executor-handler.d.ts.map +1 -1
  86. package/dist/server/express/awcp-executor-handler.js +3 -1
  87. package/dist/server/express/awcp-executor-handler.js.map +1 -1
  88. package/dist/utils/fs-helpers.d.ts +1 -1
  89. package/dist/utils/fs-helpers.js +1 -1
  90. package/package.json +2 -2
  91. package/dist/delegator/environment-builder.js.map +0 -1
  92. package/dist/delegator/snapshot-store.d.ts +0 -27
  93. package/dist/delegator/snapshot-store.d.ts.map +0 -1
  94. package/dist/delegator/snapshot-store.js +0 -40
  95. 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, 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(',')}])`);
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
- 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;
219
+ catch (error) {
220
+ stream?.abort();
221
+ await this.transport.detach(delegation.id).catch(() => { });
222
+ throw error;
122
223
  }
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,
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,
140
244
  };
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
- 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 subscribeToTaskEvents(delegationId, executorUrl) {
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
- console.log(`[AWCP:Delegator] Subscribing to SSE for ${delegationId}`);
151
- for await (const event of this.executorClient.subscribeTask(executorUrl, delegationId)) {
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 subscription error for ${delegationId}:`, error);
161
- const delegation = this.delegations.get(delegationId);
162
- if (delegation && !['completed', 'error', 'cancelled'].includes(delegation.state)) {
163
- delegation.state = 'error';
164
- delegation.error = {
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 : 'SSE subscription failed',
287
+ message: `SSE connection lost: ${error instanceof Error ? error.message : 'unknown error'}`,
167
288
  };
168
- this.delegations.set(delegationId, delegation);
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
- const executorUrl = this.executorUrls.get(delegationId);
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
- if (!this.environmentBuilder.get(delegationId))
256
- return;
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.transitionState(message.delegationId, { type: 'RECEIVE_ERROR', message });
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
- const executorUrl = this.executorUrls.get(delegationId);
335
- const result = this.transitionState(delegationId, { type: 'CANCEL' });
336
- if (!result.success) {
337
- throw new Error(`Cannot cancel delegation in state ${delegation.state}`);
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 = delegation.snapshots?.find(s => s.id === snapshotId);
356
- if (!snapshot)
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
- const snapshot = delegation.snapshots?.find(s => s.id === snapshotId);
377
- if (!snapshot)
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
- stop() {
418
- if (this.cleanupTimer) {
419
- clearInterval(this.cleanupTimer);
420
- this.cleanupTimer = undefined;
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 (!['completed', 'error', 'cancelled'].includes(delegation.state))
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 > (policy.retentionMs ?? this.config.snapshot.retentionMs)) {
444
- 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);
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 = sm.getState();
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
- return result;
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)