@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
@@ -2,144 +2,240 @@
2
2
  * AWCP Executor Service
3
3
  */
4
4
  import { EventEmitter } from 'node:events';
5
- import { generateSnapshotId, PROTOCOL_VERSION, ErrorCodes, AwcpError, CancelledError, } from '@awcp/core';
5
+ import { join } from 'node:path';
6
+ import { AssignmentStateMachine, isTerminalAssignmentState, generateSnapshotId, createAssignment, PROTOCOL_VERSION, ErrorCodes, AwcpError, CancelledError, } from '@awcp/core';
6
7
  import { resolveExecutorConfig } from './config.js';
8
+ import { AdmissionController } from './admission.js';
9
+ import { AssignmentManager } from './assignment-manager.js';
7
10
  import { WorkspaceManager } from './workspace-manager.js';
8
11
  export class ExecutorService {
9
- executor;
10
12
  config;
11
13
  transport;
12
- workspace;
13
- pendingInvitations = new Map();
14
- activeDelegations = new Map();
15
- completedDelegations = new Map();
14
+ executor;
15
+ admissionController;
16
+ workspaceManager;
17
+ assignmentManager;
18
+ assignments = new Map();
19
+ stateMachines = new Map();
20
+ eventEmitters = new Map();
21
+ cleanupTimer;
16
22
  constructor(options) {
17
- this.executor = options.executor;
18
23
  this.config = resolveExecutorConfig(options.config);
19
24
  this.transport = this.config.transport;
20
- this.workspace = new WorkspaceManager(this.config.workDir);
25
+ this.executor = options.executor;
26
+ this.admissionController = new AdmissionController(this.config.admission);
27
+ this.workspaceManager = new WorkspaceManager(this.config.workDir);
28
+ this.assignmentManager = new AssignmentManager({
29
+ baseDir: join(this.config.workDir, '.awcp', 'assignments'),
30
+ });
21
31
  }
22
- async handleMessage(message) {
23
- switch (message.type) {
24
- case 'INVITE':
25
- return this.handleInvite(message);
26
- case 'START':
27
- await this.handleStart(message);
28
- return null;
29
- case 'ERROR':
30
- await this.handleError(message);
31
- return null;
32
- default:
33
- throw new Error(`Unexpected message type: ${message.type}`);
32
+ async initialize() {
33
+ await this.transport.initialize?.(this.config.workDir);
34
+ if (this.config.cleanupOnInitialize) {
35
+ const persistedAssignments = await this.assignmentManager.loadAll();
36
+ for (const assignment of persistedAssignments) {
37
+ await this.transport.release({ delegationId: assignment.id, localPath: assignment.workPath }).catch(() => { });
38
+ await this.workspaceManager.release(assignment.workPath);
39
+ await this.assignmentManager.delete(assignment.id).catch(() => { });
40
+ }
41
+ this.startCleanupTimer();
42
+ return;
43
+ }
44
+ const persistedAssignments = await this.assignmentManager.loadAll();
45
+ const knownIds = new Set(persistedAssignments.map(a => a.id));
46
+ for (const assignment of persistedAssignments) {
47
+ this.assignments.set(assignment.id, assignment);
48
+ this.stateMachines.set(assignment.id, new AssignmentStateMachine(assignment.state));
49
+ if (!isTerminalAssignmentState(assignment.state)) {
50
+ this.eventEmitters.set(assignment.id, new EventEmitter());
51
+ }
34
52
  }
53
+ await this.workspaceManager.cleanupStale(knownIds);
54
+ this.startCleanupTimer();
35
55
  }
36
- /**
37
- * Subscribe to task events via SSE
38
- */
39
- subscribeTask(delegationId, callback) {
40
- const delegation = this.activeDelegations.get(delegationId);
41
- if (!delegation) {
42
- const errorEvent = {
43
- delegationId,
44
- type: 'error',
45
- timestamp: new Date().toISOString(),
46
- code: 'NOT_FOUND',
47
- message: 'Delegation not found',
48
- };
49
- callback(errorEvent);
50
- return () => { };
56
+ async shutdown() {
57
+ if (this.cleanupTimer) {
58
+ clearInterval(this.cleanupTimer);
59
+ this.cleanupTimer = undefined;
60
+ }
61
+ await this.transport.shutdown?.();
62
+ this.assignments.clear();
63
+ this.stateMachines.clear();
64
+ this.eventEmitters.clear();
65
+ }
66
+ async handleMessage(message) {
67
+ try {
68
+ switch (message.type) {
69
+ case 'INVITE':
70
+ return await this.handleInvite(message);
71
+ case 'START':
72
+ await this.handleStart(message);
73
+ return null;
74
+ case 'ERROR':
75
+ await this.handleError(message);
76
+ return null;
77
+ default:
78
+ throw new Error(`Unexpected message type: ${message.type}`);
79
+ }
80
+ }
81
+ catch (error) {
82
+ if (error instanceof AwcpError) {
83
+ return this.createErrorMessage(message.delegationId, error.code, error.message, error.hint);
84
+ }
85
+ throw error;
51
86
  }
52
- const handler = (event) => callback(event);
53
- delegation.eventEmitter.on('event', handler);
54
- return () => {
55
- delegation.eventEmitter.off('event', handler);
56
- };
57
87
  }
58
88
  async handleInvite(invite) {
59
89
  const { delegationId } = invite;
60
- if (this.activeDelegations.size >= this.config.policy.maxConcurrentDelegations) {
61
- return this.createErrorMessage(delegationId, ErrorCodes.DECLINED, 'Maximum concurrent delegations reached', 'Try again later when current tasks complete');
90
+ const existing = this.assignments.get(delegationId);
91
+ if (existing) {
92
+ await this.transport.detach({ delegationId, localPath: existing.workPath }).catch(() => { });
93
+ this.eventEmitters.delete(delegationId);
94
+ const retentionMs = Math.min(invite.retentionMs, this.config.assignment.maxRetentionMs);
95
+ existing.state = 'pending';
96
+ existing.invite = invite;
97
+ existing.retentionMs = retentionMs;
98
+ this.stateMachines.set(delegationId, new AssignmentStateMachine());
99
+ this.eventEmitters.set(delegationId, new EventEmitter());
100
+ await this.persistAssignment(delegationId);
101
+ console.log(`[AWCP:Executor] Re-accepting delegation ${delegationId} (was ${existing.state})`);
102
+ return {
103
+ version: PROTOCOL_VERSION,
104
+ type: 'ACCEPT',
105
+ delegationId,
106
+ retentionMs,
107
+ executorWorkDir: { path: existing.workPath },
108
+ executorConstraints: {
109
+ acceptedAccessMode: invite.lease.accessMode,
110
+ maxTtlSeconds: Math.min(invite.lease.ttlSeconds, this.config.admission.maxTtlSeconds),
111
+ sandboxProfile: this.config.assignment.sandbox,
112
+ },
113
+ };
62
114
  }
63
- const maxTtl = this.config.policy.maxTtlSeconds;
64
- if (invite.lease.ttlSeconds > maxTtl) {
65
- return this.createErrorMessage(delegationId, ErrorCodes.DECLINED, `Requested TTL (${invite.lease.ttlSeconds}s) exceeds maximum (${maxTtl}s)`, `Request a shorter TTL (max: ${maxTtl}s)`);
115
+ await this.admissionController.check({ invite, assignments: this.assignments, transport: this.transport });
116
+ await this.config.hooks.onAdmissionCheck?.(invite);
117
+ const retentionMs = Math.min(invite.retentionMs, this.config.assignment.maxRetentionMs);
118
+ const workPath = this.workspaceManager.allocate(delegationId);
119
+ try {
120
+ const assignment = createAssignment({ id: delegationId, invite, workPath, retentionMs });
121
+ this.assignments.set(delegationId, assignment);
122
+ this.stateMachines.set(delegationId, new AssignmentStateMachine());
123
+ this.eventEmitters.set(delegationId, new EventEmitter());
124
+ await this.persistAssignment(delegationId);
125
+ const executorConstraints = {
126
+ acceptedAccessMode: invite.lease.accessMode,
127
+ maxTtlSeconds: Math.min(invite.lease.ttlSeconds, this.config.admission.maxTtlSeconds),
128
+ sandboxProfile: this.config.assignment.sandbox,
129
+ };
130
+ return {
131
+ version: PROTOCOL_VERSION,
132
+ type: 'ACCEPT',
133
+ delegationId,
134
+ retentionMs,
135
+ executorWorkDir: { path: workPath },
136
+ executorConstraints,
137
+ };
66
138
  }
67
- const allowedModes = this.config.policy.allowedAccessModes;
68
- if (!allowedModes.includes(invite.lease.accessMode)) {
69
- return this.createErrorMessage(delegationId, ErrorCodes.DECLINED, `Access mode '${invite.lease.accessMode}' not allowed`, `Allowed modes: ${allowedModes.join(', ')}`);
139
+ catch (error) {
140
+ this.assignments.delete(delegationId);
141
+ this.stateMachines.delete(delegationId);
142
+ this.eventEmitters.delete(delegationId);
143
+ await this.workspaceManager.release(workPath);
144
+ await this.assignmentManager.delete(delegationId).catch(() => { });
145
+ throw error;
70
146
  }
71
- const depCheck = await this.transport.checkDependency();
72
- if (!depCheck.available) {
73
- return this.createErrorMessage(delegationId, ErrorCodes.DEP_MISSING, `Transport ${this.transport.type} is not available`, depCheck.hint);
147
+ }
148
+ async handleStart(start) {
149
+ const { delegationId } = start;
150
+ const assignment = this.assignments.get(delegationId);
151
+ if (!assignment) {
152
+ throw new Error(`Unknown delegation for START: ${delegationId}` +
153
+ ` (known=[${Array.from(this.assignments.keys()).join(',')}])`);
74
154
  }
75
- if (this.config.hooks.onInvite) {
76
- const accepted = await this.config.hooks.onInvite(invite);
77
- if (!accepted) {
78
- return this.createErrorMessage(delegationId, ErrorCodes.DECLINED, 'Invitation declined by policy', 'The agent declined this delegation request');
79
- }
155
+ this.transitionState(delegationId, { type: 'RECEIVE_START' });
156
+ assignment.lease = start.lease;
157
+ assignment.startedAt = new Date().toISOString();
158
+ await this.persistAssignment(delegationId);
159
+ console.log(`[AWCP:Executor] Delegation ${delegationId} started` +
160
+ ` (active=${Array.from(this.assignments.values()).filter(a => a.state === 'active').length}, workPath=${assignment.workPath})`);
161
+ this.executeTask(delegationId, start);
162
+ }
163
+ async handleError(error) {
164
+ const { delegationId } = error;
165
+ const assignment = this.assignments.get(delegationId);
166
+ if (!assignment) {
167
+ throw new Error(`Unknown delegation for ERROR: ${delegationId}` +
168
+ ` (known=[${Array.from(this.assignments.keys()).join(',')}])`);
80
169
  }
81
- else if (!this.config.policy.autoAccept) {
82
- this.pendingInvitations.set(delegationId, {
83
- invite,
84
- receivedAt: new Date(),
85
- });
86
- return this.createErrorMessage(delegationId, ErrorCodes.DECLINED, 'Manual acceptance required but no hook provided', 'Configure autoAccept: true or provide onInvite hook');
170
+ this.transitionState(delegationId, { type: 'RECEIVE_ERROR' });
171
+ console.log(`[AWCP:Executor] Received ERROR for ${delegationId}: ${error.code} - ${error.message}`);
172
+ assignment.completedAt = new Date().toISOString();
173
+ assignment.error = { code: error.code, message: error.message, hint: error.hint };
174
+ await this.persistAssignment(delegationId);
175
+ this.config.hooks.onError?.(delegationId, new AwcpError(error.code, error.message, error.hint, delegationId));
176
+ }
177
+ subscribeTask(delegationId, callback) {
178
+ const assignment = this.assignments.get(delegationId);
179
+ if (!assignment) {
180
+ console.error(`[AWCP:Executor] SSE subscribe rejected for ${delegationId}: unknown delegation`);
181
+ const errorEvent = {
182
+ delegationId, type: 'error', timestamp: new Date().toISOString(),
183
+ code: 'NOT_FOUND', message: 'Delegation not found on executor',
184
+ };
185
+ callback(errorEvent);
186
+ return () => { };
187
+ }
188
+ const sm = this.stateMachines.get(delegationId);
189
+ if (sm.isTerminal()) {
190
+ console.log(`[AWCP:Executor] SSE reconnect for ${delegationId}, replaying ${assignment.state} event`);
191
+ const event = assignment.state === 'completed'
192
+ ? {
193
+ delegationId, type: 'done', timestamp: assignment.completedAt,
194
+ summary: assignment.result?.summary ?? 'Task completed',
195
+ highlights: assignment.result?.highlights,
196
+ }
197
+ : {
198
+ delegationId, type: 'error', timestamp: assignment.completedAt,
199
+ code: assignment.error?.code ?? ErrorCodes.TASK_FAILED,
200
+ message: assignment.error?.message ?? 'Task failed',
201
+ hint: assignment.error?.hint,
202
+ };
203
+ setImmediate(() => callback(event));
204
+ return () => { };
87
205
  }
88
- const workPath = this.workspace.allocate(delegationId);
89
- const validation = this.workspace.validate(workPath);
90
- if (!validation.valid) {
91
- await this.workspace.release(workPath);
92
- return this.createErrorMessage(delegationId, ErrorCodes.WORKDIR_DENIED, validation.reason ?? 'Workspace validation failed', 'Check workDir configuration');
206
+ const emitter = this.eventEmitters.get(delegationId);
207
+ if (!emitter) {
208
+ console.error(`[AWCP:Executor] SSE subscribe failed for ${delegationId}: no event emitter`);
209
+ return () => { };
93
210
  }
94
- this.pendingInvitations.set(delegationId, {
95
- invite,
96
- receivedAt: new Date(),
97
- });
98
- const executorConstraints = {
99
- acceptedAccessMode: invite.lease.accessMode,
100
- maxTtlSeconds: Math.min(invite.lease.ttlSeconds, maxTtl),
101
- sandboxProfile: this.config.sandbox,
102
- };
103
- const acceptMessage = {
104
- version: PROTOCOL_VERSION,
105
- type: 'ACCEPT',
106
- delegationId,
107
- executorWorkDir: { path: workPath },
108
- executorConstraints,
211
+ console.log(`[AWCP:Executor] SSE subscriber attached for ${delegationId} (state=${assignment.state})`);
212
+ const handler = (event) => callback(event);
213
+ emitter.on('event', handler);
214
+ return () => {
215
+ console.log(`[AWCP:Executor] SSE subscriber detached for ${delegationId}`);
216
+ emitter.off('event', handler);
109
217
  };
110
- return acceptMessage;
111
- }
112
- async handleStart(start) {
113
- const { delegationId } = start;
114
- const pending = this.pendingInvitations.get(delegationId);
115
- if (!pending) {
116
- console.warn(`[AWCP:Executor] Unknown delegation for START: ${delegationId}`);
117
- return;
118
- }
119
- const workPath = this.workspace.allocate(delegationId);
120
- this.pendingInvitations.delete(delegationId);
121
- const eventEmitter = new EventEmitter();
122
- this.activeDelegations.set(delegationId, {
123
- id: delegationId,
124
- workPath,
125
- task: pending.invite.task,
126
- lease: start.lease,
127
- environment: pending.invite.environment,
128
- startedAt: new Date(),
129
- eventEmitter,
130
- });
131
- // Task execution runs async - don't await
132
- this.executeTask(delegationId, start, workPath, pending.invite.task, start.lease, pending.invite.environment, eventEmitter);
133
218
  }
134
- async executeTask(delegationId, start, workPath, task, lease, environment, eventEmitter) {
219
+ async executeTask(delegationId, start) {
220
+ const assignment = this.assignments.get(delegationId);
221
+ const emitter = this.eventEmitters.get(delegationId);
135
222
  try {
136
- await this.workspace.prepare(workPath);
223
+ console.log(`[AWCP:Executor] Task ${delegationId} preparing workspace...`);
224
+ await this.workspaceManager.prepare(assignment.workPath);
225
+ console.log(`[AWCP:Executor] Task ${delegationId} setting up transport (${this.transport.type})...`);
137
226
  const actualPath = await this.transport.setup({
138
227
  delegationId,
139
- workDirInfo: start.workDir,
140
- workDir: workPath,
228
+ handle: start.transportHandle,
229
+ localPath: assignment.workPath,
230
+ });
231
+ this.config.hooks.onTaskStart?.({
232
+ delegationId,
233
+ workPath: actualPath,
234
+ task: assignment.invite.task,
235
+ lease: assignment.lease,
236
+ environment: assignment.invite.environment,
141
237
  });
142
- this.config.hooks.onTaskStart?.({ delegationId, workPath: actualPath, task, lease, environment });
238
+ console.log(`[AWCP:Executor] Task ${delegationId} executing (listeners=${emitter.listenerCount('event')})...`);
143
239
  const statusEvent = {
144
240
  delegationId,
145
241
  type: 'status',
@@ -147,16 +243,17 @@ export class ExecutorService {
147
243
  status: 'running',
148
244
  message: 'Task execution started',
149
245
  };
150
- eventEmitter.emit('event', statusEvent);
246
+ emitter.emit('event', statusEvent);
151
247
  const result = await this.executor.execute({
152
248
  delegationId,
153
249
  workPath: actualPath,
154
- task,
155
- environment,
250
+ task: assignment.invite.task,
251
+ environment: assignment.invite.environment,
156
252
  });
157
- const teardownResult = await this.transport.teardown({ delegationId, workDir: actualPath });
253
+ console.log(`[AWCP:Executor] Task ${delegationId} completed, capturing snapshot...`);
254
+ const snapshotResult = await this.transport.captureSnapshot?.({ delegationId, localPath: actualPath });
158
255
  const snapshotId = generateSnapshotId();
159
- if (teardownResult.snapshotBase64) {
256
+ if (snapshotResult?.snapshotBase64) {
160
257
  const snapshotEvent = {
161
258
  delegationId,
162
259
  type: 'snapshot',
@@ -164,10 +261,10 @@ export class ExecutorService {
164
261
  snapshotId,
165
262
  summary: result.summary,
166
263
  highlights: result.highlights,
167
- snapshotBase64: teardownResult.snapshotBase64,
264
+ snapshotBase64: snapshotResult.snapshotBase64,
168
265
  recommended: true,
169
266
  };
170
- eventEmitter.emit('event', snapshotEvent);
267
+ emitter.emit('event', snapshotEvent);
171
268
  }
172
269
  const doneEvent = {
173
270
  delegationId,
@@ -175,32 +272,25 @@ export class ExecutorService {
175
272
  timestamp: new Date().toISOString(),
176
273
  summary: result.summary,
177
274
  highlights: result.highlights,
178
- snapshotIds: teardownResult.snapshotBase64 ? [snapshotId] : undefined,
179
- recommendedSnapshotId: teardownResult.snapshotBase64 ? snapshotId : undefined,
275
+ snapshotIds: snapshotResult?.snapshotBase64 ? [snapshotId] : undefined,
276
+ recommendedSnapshotId: snapshotResult?.snapshotBase64 ? snapshotId : undefined,
180
277
  };
181
- eventEmitter.emit('event', doneEvent);
278
+ emitter.emit('event', doneEvent);
279
+ console.log(`[AWCP:Executor] Task ${delegationId} done event emitted` +
280
+ ` (listeners=${emitter.listenerCount('event')})`);
182
281
  this.config.hooks.onTaskComplete?.(delegationId, result.summary);
183
- this.completedDelegations.set(delegationId, {
184
- id: delegationId,
185
- completedAt: new Date(),
186
- state: 'completed',
187
- snapshot: teardownResult.snapshotBase64 ? {
188
- id: snapshotId,
189
- summary: result.summary,
190
- highlights: result.highlights,
191
- snapshotBase64: teardownResult.snapshotBase64,
192
- } : undefined,
193
- });
194
- this.scheduleResultCleanup(delegationId);
195
- this.activeDelegations.delete(delegationId);
196
- await this.workspace.release(actualPath);
282
+ this.transitionState(delegationId, { type: 'TASK_COMPLETE' });
283
+ assignment.completedAt = new Date().toISOString();
284
+ assignment.result = {
285
+ summary: result.summary,
286
+ highlights: result.highlights,
287
+ snapshotBase64: snapshotResult?.snapshotBase64,
288
+ };
289
+ await this.persistAssignment(delegationId);
290
+ await this.transport.detach({ delegationId, localPath: assignment.workPath }).catch(() => { });
197
291
  }
198
292
  catch (error) {
199
- const delegation = this.activeDelegations.get(delegationId);
200
- if (delegation) {
201
- await this.transport.teardown({ delegationId, workDir: delegation.workPath }).catch(() => { });
202
- await this.workspace.release(delegation.workPath);
203
- }
293
+ console.error(`[AWCP:Executor] Task ${delegationId} failed:`, error instanceof Error ? error.message : error);
204
294
  const errorEvent = {
205
295
  delegationId,
206
296
  type: 'error',
@@ -209,37 +299,30 @@ export class ExecutorService {
209
299
  message: error instanceof Error ? error.message : String(error),
210
300
  hint: 'Check task requirements and try again',
211
301
  };
212
- eventEmitter.emit('event', errorEvent);
302
+ emitter.emit('event', errorEvent);
303
+ console.log(`[AWCP:Executor] Task ${delegationId} error event emitted` +
304
+ ` (listeners=${emitter.listenerCount('event')})`);
213
305
  this.config.hooks.onError?.(delegationId, error instanceof Error ? error : new Error(String(error)));
214
- this.completedDelegations.set(delegationId, {
215
- id: delegationId,
216
- completedAt: new Date(),
217
- state: 'error',
218
- error: {
219
- code: ErrorCodes.TASK_FAILED,
220
- message: error instanceof Error ? error.message : String(error),
221
- hint: 'Check task requirements and try again',
222
- },
223
- });
224
- this.scheduleResultCleanup(delegationId);
225
- this.activeDelegations.delete(delegationId);
226
- }
227
- }
228
- async handleError(error) {
229
- const { delegationId } = error;
230
- const delegation = this.activeDelegations.get(delegationId);
231
- if (delegation) {
232
- await this.transport.teardown({ delegationId, workDir: delegation.workPath }).catch(() => { });
233
- this.activeDelegations.delete(delegationId);
234
- await this.workspace.release(delegation.workPath);
306
+ this.transitionState(delegationId, { type: 'TASK_FAIL' });
307
+ assignment.completedAt = new Date().toISOString();
308
+ assignment.error = {
309
+ code: ErrorCodes.TASK_FAILED,
310
+ message: error instanceof Error ? error.message : String(error),
311
+ hint: 'Check task requirements and try again',
312
+ };
313
+ await this.persistAssignment(delegationId);
314
+ await this.transport.detach({ delegationId, localPath: assignment.workPath }).catch(() => { });
235
315
  }
236
- this.pendingInvitations.delete(delegationId);
237
- this.config.hooks.onError?.(delegationId, new AwcpError(error.code, error.message, error.hint, delegationId));
238
316
  }
239
317
  async cancelDelegation(delegationId) {
240
- const delegation = this.activeDelegations.get(delegationId);
241
- if (delegation) {
242
- await this.transport.teardown({ delegationId, workDir: delegation.workPath }).catch(() => { });
318
+ const assignment = this.assignments.get(delegationId);
319
+ if (!assignment) {
320
+ throw new Error(`Delegation not found: ${delegationId}`);
321
+ }
322
+ this.transitionState(delegationId, { type: 'CANCEL' });
323
+ console.log(`[AWCP:Executor] Cancelling delegation ${delegationId}`);
324
+ const emitter = this.eventEmitters.get(delegationId);
325
+ if (emitter) {
243
326
  const errorEvent = {
244
327
  delegationId,
245
328
  type: 'error',
@@ -247,67 +330,98 @@ export class ExecutorService {
247
330
  code: ErrorCodes.CANCELLED,
248
331
  message: 'Delegation cancelled',
249
332
  };
250
- delegation.eventEmitter.emit('event', errorEvent);
251
- this.activeDelegations.delete(delegationId);
252
- await this.workspace.release(delegation.workPath);
253
- this.config.hooks.onError?.(delegationId, new CancelledError('Delegation cancelled by Delegator', undefined, delegationId));
254
- return;
333
+ emitter.emit('event', errorEvent);
255
334
  }
256
- if (this.pendingInvitations.has(delegationId)) {
257
- this.pendingInvitations.delete(delegationId);
258
- return;
259
- }
260
- throw new Error(`Delegation not found: ${delegationId}`);
335
+ assignment.completedAt = new Date().toISOString();
336
+ assignment.error = { code: ErrorCodes.CANCELLED, message: 'Delegation cancelled' };
337
+ await this.persistAssignment(delegationId);
338
+ await this.transport.detach({ delegationId, localPath: assignment.workPath }).catch(() => { });
339
+ this.config.hooks.onError?.(delegationId, new CancelledError('Delegation cancelled by Delegator', undefined, delegationId));
261
340
  }
262
341
  getStatus() {
342
+ const active = Array.from(this.assignments.values()).filter(a => a.state === 'active');
263
343
  return {
264
- pendingInvitations: this.pendingInvitations.size,
265
- activeDelegations: this.activeDelegations.size,
266
- completedDelegations: this.completedDelegations.size,
267
- delegations: Array.from(this.activeDelegations.values()).map((d) => ({
268
- id: d.id,
269
- workPath: d.workPath,
270
- startedAt: d.startedAt.toISOString(),
344
+ pendingInvitations: Array.from(this.assignments.values()).filter(a => a.state === 'pending').length,
345
+ activeDelegations: active.length,
346
+ completedDelegations: Array.from(this.assignments.values()).filter(a => a.state === 'completed' || a.state === 'error').length,
347
+ delegations: active.map((a) => ({
348
+ id: a.id,
349
+ workPath: a.workPath,
350
+ startedAt: a.startedAt,
271
351
  })),
272
352
  };
273
353
  }
274
354
  getTaskResult(delegationId) {
275
- const active = this.activeDelegations.get(delegationId);
276
- if (active) {
277
- return { status: 'running' };
278
- }
279
- const completed = this.completedDelegations.get(delegationId);
280
- if (completed) {
281
- if (completed.state === 'completed') {
282
- return {
283
- status: 'completed',
284
- completedAt: completed.completedAt.toISOString(),
285
- summary: completed.snapshot?.summary,
286
- highlights: completed.snapshot?.highlights,
287
- snapshotBase64: completed.snapshot?.snapshotBase64,
288
- };
355
+ const assignment = this.assignments.get(delegationId);
356
+ if (!assignment) {
357
+ if (this.transport.type === 'sshfs') {
358
+ return { status: 'not_applicable', reason: 'SSHFS transport writes directly to source' };
289
359
  }
290
- return {
291
- status: 'error',
292
- completedAt: completed.completedAt.toISOString(),
293
- error: completed.error,
294
- };
360
+ return { status: 'not_found' };
295
361
  }
296
- if (this.transport.type === 'sshfs') {
362
+ const sm = this.stateMachines.get(delegationId);
363
+ if (!sm.isTerminal()) {
364
+ return { status: 'running' };
365
+ }
366
+ if (assignment.state === 'completed') {
297
367
  return {
298
- status: 'not_applicable',
299
- reason: 'SSHFS transport writes directly to source',
368
+ status: 'completed',
369
+ completedAt: assignment.completedAt,
370
+ summary: assignment.result?.summary,
371
+ highlights: assignment.result?.highlights,
372
+ snapshotBase64: assignment.result?.snapshotBase64,
300
373
  };
301
374
  }
302
- return { status: 'not_found' };
375
+ return {
376
+ status: 'error',
377
+ completedAt: assignment.completedAt,
378
+ error: assignment.error,
379
+ };
303
380
  }
304
381
  acknowledgeResult(delegationId) {
305
- this.completedDelegations.delete(delegationId);
382
+ const assignment = this.assignments.get(delegationId);
383
+ const sm = this.stateMachines.get(delegationId);
384
+ if (assignment && sm?.isTerminal()) {
385
+ this.assignments.delete(delegationId);
386
+ this.stateMachines.delete(delegationId);
387
+ this.eventEmitters.delete(delegationId);
388
+ this.assignmentManager.delete(delegationId).catch(() => { });
389
+ }
390
+ }
391
+ transitionState(delegationId, event) {
392
+ const sm = this.stateMachines.get(delegationId);
393
+ const assignment = this.assignments.get(delegationId);
394
+ const result = sm.transition(event);
395
+ if (!result.success) {
396
+ throw new Error(`Cannot transition assignment ${delegationId} (${event.type}) in state '${assignment.state}': ${result.error}`);
397
+ }
398
+ assignment.state = sm.getState();
399
+ assignment.updatedAt = new Date().toISOString();
306
400
  }
307
- scheduleResultCleanup(delegationId) {
308
- setTimeout(() => {
309
- this.completedDelegations.delete(delegationId);
310
- }, this.config.policy.resultRetentionMs);
401
+ async persistAssignment(delegationId) {
402
+ const assignment = this.assignments.get(delegationId);
403
+ if (assignment) {
404
+ await this.assignmentManager.save(assignment);
405
+ }
406
+ }
407
+ startCleanupTimer() {
408
+ this.cleanupTimer = setInterval(async () => {
409
+ const now = Date.now();
410
+ for (const [id, assignment] of this.assignments) {
411
+ const sm = this.stateMachines.get(id);
412
+ if (!sm?.isTerminal())
413
+ continue;
414
+ const updatedAt = new Date(assignment.updatedAt).getTime();
415
+ if (now - updatedAt > assignment.retentionMs) {
416
+ await this.transport.release({ delegationId: id, localPath: assignment.workPath }).catch(() => { });
417
+ await this.workspaceManager.release(assignment.workPath);
418
+ await this.assignmentManager.delete(id).catch(() => { });
419
+ this.assignments.delete(id);
420
+ this.stateMachines.delete(id);
421
+ this.eventEmitters.delete(id);
422
+ }
423
+ }
424
+ }, 60 * 1000);
311
425
  }
312
426
  createErrorMessage(delegationId, code, message, hint) {
313
427
  return {