@awcp/sdk 0.0.17 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +4 -3
  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
@@ -2,160 +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 completed = this.completedDelegations.get(delegationId);
43
- const activeIds = Array.from(this.activeDelegations.keys());
44
- const completedIds = Array.from(this.completedDelegations.keys());
45
- const reason = completed
46
- ? `delegation already ${completed.state} at ${completed.completedAt.toISOString()}`
47
- : `delegation unknown to this executor instance`;
48
- console.error(`[AWCP:Executor] SSE subscribe rejected for ${delegationId}: ${reason}` +
49
- ` (active=[${activeIds.join(',')}], completed=[${completedIds.join(',')}])`);
50
- const errorEvent = {
51
- delegationId,
52
- type: 'error',
53
- timestamp: new Date().toISOString(),
54
- code: 'NOT_FOUND',
55
- message: `Delegation not found on executor: ${reason}`,
56
- };
57
- callback(errorEvent);
58
- 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;
59
86
  }
60
- console.log(`[AWCP:Executor] SSE subscriber attached for ${delegationId}`);
61
- const handler = (event) => callback(event);
62
- delegation.eventEmitter.on('event', handler);
63
- return () => {
64
- console.log(`[AWCP:Executor] SSE subscriber detached for ${delegationId}`);
65
- delegation.eventEmitter.off('event', handler);
66
- };
67
87
  }
68
88
  async handleInvite(invite) {
69
89
  const { delegationId } = invite;
70
- if (this.activeDelegations.size >= this.config.policy.maxConcurrentDelegations) {
71
- 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
+ };
114
+ }
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
+ };
72
138
  }
73
- const maxTtl = this.config.policy.maxTtlSeconds;
74
- if (invite.lease.ttlSeconds > maxTtl) {
75
- return this.createErrorMessage(delegationId, ErrorCodes.DECLINED, `Requested TTL (${invite.lease.ttlSeconds}s) exceeds maximum (${maxTtl}s)`, `Request a shorter TTL (max: ${maxTtl}s)`);
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;
76
146
  }
77
- const allowedModes = this.config.policy.allowedAccessModes;
78
- if (!allowedModes.includes(invite.lease.accessMode)) {
79
- return this.createErrorMessage(delegationId, ErrorCodes.DECLINED, `Access mode '${invite.lease.accessMode}' not allowed`, `Allowed modes: ${allowedModes.join(', ')}`);
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(',')}])`);
80
154
  }
81
- const depCheck = await this.transport.checkDependency();
82
- if (!depCheck.available) {
83
- return this.createErrorMessage(delegationId, ErrorCodes.DEP_MISSING, `Transport ${this.transport.type} is not available`, depCheck.hint);
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(',')}])`);
84
169
  }
85
- if (this.config.hooks.onInvite) {
86
- const accepted = await this.config.hooks.onInvite(invite);
87
- if (!accepted) {
88
- return this.createErrorMessage(delegationId, ErrorCodes.DECLINED, 'Invitation declined by policy', 'The agent declined this delegation request');
89
- }
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 () => { };
90
187
  }
91
- else if (!this.config.policy.autoAccept) {
92
- this.pendingInvitations.set(delegationId, {
93
- invite,
94
- receivedAt: new Date(),
95
- });
96
- return this.createErrorMessage(delegationId, ErrorCodes.DECLINED, 'Manual acceptance required but no hook provided', 'Configure autoAccept: true or provide onInvite hook');
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 () => { };
97
205
  }
98
- const workPath = this.workspace.allocate(delegationId);
99
- const validation = this.workspace.validate(workPath);
100
- if (!validation.valid) {
101
- await this.workspace.release(workPath);
102
- 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 () => { };
103
210
  }
104
- this.pendingInvitations.set(delegationId, {
105
- invite,
106
- receivedAt: new Date(),
107
- });
108
- const executorConstraints = {
109
- acceptedAccessMode: invite.lease.accessMode,
110
- maxTtlSeconds: Math.min(invite.lease.ttlSeconds, maxTtl),
111
- sandboxProfile: this.config.sandbox,
112
- };
113
- const acceptMessage = {
114
- version: PROTOCOL_VERSION,
115
- type: 'ACCEPT',
116
- delegationId,
117
- executorWorkDir: { path: workPath },
118
- 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);
119
217
  };
120
- return acceptMessage;
121
- }
122
- async handleStart(start) {
123
- const { delegationId } = start;
124
- const pending = this.pendingInvitations.get(delegationId);
125
- if (!pending) {
126
- console.warn(`[AWCP:Executor] START rejected for unknown delegation ${delegationId}` +
127
- ` (pending=[${Array.from(this.pendingInvitations.keys()).join(',')}])`);
128
- return;
129
- }
130
- const workPath = this.workspace.allocate(delegationId);
131
- this.pendingInvitations.delete(delegationId);
132
- const eventEmitter = new EventEmitter();
133
- this.activeDelegations.set(delegationId, {
134
- id: delegationId,
135
- workPath,
136
- task: pending.invite.task,
137
- lease: start.lease,
138
- environment: pending.invite.environment,
139
- startedAt: new Date(),
140
- eventEmitter,
141
- });
142
- console.log(`[AWCP:Executor] Delegation ${delegationId} registered` +
143
- ` (active=${this.activeDelegations.size}, workPath=${workPath})`);
144
- // Task execution runs async - don't await
145
- this.executeTask(delegationId, start, workPath, pending.invite.task, start.lease, pending.invite.environment, eventEmitter);
146
218
  }
147
- 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);
148
222
  try {
149
223
  console.log(`[AWCP:Executor] Task ${delegationId} preparing workspace...`);
150
- await this.workspace.prepare(workPath);
224
+ await this.workspaceManager.prepare(assignment.workPath);
151
225
  console.log(`[AWCP:Executor] Task ${delegationId} setting up transport (${this.transport.type})...`);
152
226
  const actualPath = await this.transport.setup({
153
227
  delegationId,
154
- workDirInfo: start.workDir,
155
- workDir: workPath,
228
+ handle: start.transportHandle,
229
+ localPath: assignment.workPath,
156
230
  });
157
- this.config.hooks.onTaskStart?.({ delegationId, workPath: actualPath, task, lease, environment });
158
- console.log(`[AWCP:Executor] Task ${delegationId} executing (listeners=${eventEmitter.listenerCount('event')})...`);
231
+ this.config.hooks.onTaskStart?.({
232
+ delegationId,
233
+ workPath: actualPath,
234
+ task: assignment.invite.task,
235
+ lease: assignment.lease,
236
+ environment: assignment.invite.environment,
237
+ });
238
+ console.log(`[AWCP:Executor] Task ${delegationId} executing (listeners=${emitter.listenerCount('event')})...`);
159
239
  const statusEvent = {
160
240
  delegationId,
161
241
  type: 'status',
@@ -163,17 +243,17 @@ export class ExecutorService {
163
243
  status: 'running',
164
244
  message: 'Task execution started',
165
245
  };
166
- eventEmitter.emit('event', statusEvent);
246
+ emitter.emit('event', statusEvent);
167
247
  const result = await this.executor.execute({
168
248
  delegationId,
169
249
  workPath: actualPath,
170
- task,
171
- environment,
250
+ task: assignment.invite.task,
251
+ environment: assignment.invite.environment,
172
252
  });
173
- console.log(`[AWCP:Executor] Task ${delegationId} completed, tearing down transport...`);
174
- 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 });
175
255
  const snapshotId = generateSnapshotId();
176
- if (teardownResult.snapshotBase64) {
256
+ if (snapshotResult?.snapshotBase64) {
177
257
  const snapshotEvent = {
178
258
  delegationId,
179
259
  type: 'snapshot',
@@ -181,10 +261,10 @@ export class ExecutorService {
181
261
  snapshotId,
182
262
  summary: result.summary,
183
263
  highlights: result.highlights,
184
- snapshotBase64: teardownResult.snapshotBase64,
264
+ snapshotBase64: snapshotResult.snapshotBase64,
185
265
  recommended: true,
186
266
  };
187
- eventEmitter.emit('event', snapshotEvent);
267
+ emitter.emit('event', snapshotEvent);
188
268
  }
189
269
  const doneEvent = {
190
270
  delegationId,
@@ -192,36 +272,25 @@ export class ExecutorService {
192
272
  timestamp: new Date().toISOString(),
193
273
  summary: result.summary,
194
274
  highlights: result.highlights,
195
- snapshotIds: teardownResult.snapshotBase64 ? [snapshotId] : undefined,
196
- recommendedSnapshotId: teardownResult.snapshotBase64 ? snapshotId : undefined,
275
+ snapshotIds: snapshotResult?.snapshotBase64 ? [snapshotId] : undefined,
276
+ recommendedSnapshotId: snapshotResult?.snapshotBase64 ? snapshotId : undefined,
197
277
  };
198
- eventEmitter.emit('event', doneEvent);
278
+ emitter.emit('event', doneEvent);
199
279
  console.log(`[AWCP:Executor] Task ${delegationId} done event emitted` +
200
- ` (listeners=${eventEmitter.listenerCount('event')})`);
280
+ ` (listeners=${emitter.listenerCount('event')})`);
201
281
  this.config.hooks.onTaskComplete?.(delegationId, result.summary);
202
- this.completedDelegations.set(delegationId, {
203
- id: delegationId,
204
- completedAt: new Date(),
205
- state: 'completed',
206
- snapshot: teardownResult.snapshotBase64 ? {
207
- id: snapshotId,
208
- summary: result.summary,
209
- highlights: result.highlights,
210
- snapshotBase64: teardownResult.snapshotBase64,
211
- } : undefined,
212
- });
213
- this.scheduleResultCleanup(delegationId);
214
- console.log(`[AWCP:Executor] Delegation ${delegationId} moved to completed, removing from active`);
215
- this.activeDelegations.delete(delegationId);
216
- 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(() => { });
217
291
  }
218
292
  catch (error) {
219
293
  console.error(`[AWCP:Executor] Task ${delegationId} failed:`, error instanceof Error ? error.message : error);
220
- const delegation = this.activeDelegations.get(delegationId);
221
- if (delegation) {
222
- await this.transport.teardown({ delegationId, workDir: delegation.workPath }).catch(() => { });
223
- await this.workspace.release(delegation.workPath);
224
- }
225
294
  const errorEvent = {
226
295
  delegationId,
227
296
  type: 'error',
@@ -230,43 +299,30 @@ export class ExecutorService {
230
299
  message: error instanceof Error ? error.message : String(error),
231
300
  hint: 'Check task requirements and try again',
232
301
  };
233
- eventEmitter.emit('event', errorEvent);
302
+ emitter.emit('event', errorEvent);
234
303
  console.log(`[AWCP:Executor] Task ${delegationId} error event emitted` +
235
- ` (listeners=${eventEmitter.listenerCount('event')})`);
304
+ ` (listeners=${emitter.listenerCount('event')})`);
236
305
  this.config.hooks.onError?.(delegationId, error instanceof Error ? error : new Error(String(error)));
237
- this.completedDelegations.set(delegationId, {
238
- id: delegationId,
239
- completedAt: new Date(),
240
- state: 'error',
241
- error: {
242
- code: ErrorCodes.TASK_FAILED,
243
- message: error instanceof Error ? error.message : String(error),
244
- hint: 'Check task requirements and try again',
245
- },
246
- });
247
- this.scheduleResultCleanup(delegationId);
248
- console.log(`[AWCP:Executor] Delegation ${delegationId} moved to error state, removing from active`);
249
- this.activeDelegations.delete(delegationId);
250
- }
251
- }
252
- async handleError(error) {
253
- const { delegationId } = error;
254
- console.log(`[AWCP:Executor] Received ERROR message for ${delegationId}: ${error.code} - ${error.message}`);
255
- const delegation = this.activeDelegations.get(delegationId);
256
- if (delegation) {
257
- await this.transport.teardown({ delegationId, workDir: delegation.workPath }).catch(() => { });
258
- console.log(`[AWCP:Executor] Delegation ${delegationId} removed by delegator error`);
259
- this.activeDelegations.delete(delegationId);
260
- 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(() => { });
261
315
  }
262
- this.pendingInvitations.delete(delegationId);
263
- this.config.hooks.onError?.(delegationId, new AwcpError(error.code, error.message, error.hint, delegationId));
264
316
  }
265
317
  async cancelDelegation(delegationId) {
266
- const delegation = this.activeDelegations.get(delegationId);
267
- if (delegation) {
268
- console.log(`[AWCP:Executor] Cancelling active delegation ${delegationId}`);
269
- 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) {
270
326
  const errorEvent = {
271
327
  delegationId,
272
328
  type: 'error',
@@ -274,68 +330,98 @@ export class ExecutorService {
274
330
  code: ErrorCodes.CANCELLED,
275
331
  message: 'Delegation cancelled',
276
332
  };
277
- delegation.eventEmitter.emit('event', errorEvent);
278
- console.log(`[AWCP:Executor] Delegation ${delegationId} removed by cancellation`);
279
- this.activeDelegations.delete(delegationId);
280
- await this.workspace.release(delegation.workPath);
281
- this.config.hooks.onError?.(delegationId, new CancelledError('Delegation cancelled by Delegator', undefined, delegationId));
282
- return;
333
+ emitter.emit('event', errorEvent);
283
334
  }
284
- if (this.pendingInvitations.has(delegationId)) {
285
- this.pendingInvitations.delete(delegationId);
286
- return;
287
- }
288
- 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));
289
340
  }
290
341
  getStatus() {
342
+ const active = Array.from(this.assignments.values()).filter(a => a.state === 'active');
291
343
  return {
292
- pendingInvitations: this.pendingInvitations.size,
293
- activeDelegations: this.activeDelegations.size,
294
- completedDelegations: this.completedDelegations.size,
295
- delegations: Array.from(this.activeDelegations.values()).map((d) => ({
296
- id: d.id,
297
- workPath: d.workPath,
298
- 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,
299
351
  })),
300
352
  };
301
353
  }
302
354
  getTaskResult(delegationId) {
303
- const active = this.activeDelegations.get(delegationId);
304
- if (active) {
305
- return { status: 'running' };
306
- }
307
- const completed = this.completedDelegations.get(delegationId);
308
- if (completed) {
309
- if (completed.state === 'completed') {
310
- return {
311
- status: 'completed',
312
- completedAt: completed.completedAt.toISOString(),
313
- summary: completed.snapshot?.summary,
314
- highlights: completed.snapshot?.highlights,
315
- snapshotBase64: completed.snapshot?.snapshotBase64,
316
- };
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' };
317
359
  }
318
- return {
319
- status: 'error',
320
- completedAt: completed.completedAt.toISOString(),
321
- error: completed.error,
322
- };
360
+ return { status: 'not_found' };
323
361
  }
324
- 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') {
325
367
  return {
326
- status: 'not_applicable',
327
- 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,
328
373
  };
329
374
  }
330
- return { status: 'not_found' };
375
+ return {
376
+ status: 'error',
377
+ completedAt: assignment.completedAt,
378
+ error: assignment.error,
379
+ };
331
380
  }
332
381
  acknowledgeResult(delegationId) {
333
- 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();
334
400
  }
335
- scheduleResultCleanup(delegationId) {
336
- setTimeout(() => {
337
- this.completedDelegations.delete(delegationId);
338
- }, 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);
339
425
  }
340
426
  createErrorMessage(delegationId, code, message, hint) {
341
427
  return {