@delexec/ops 0.1.0

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 (55) hide show
  1. package/README.md +3 -0
  2. package/README.zh-CN.md +6 -0
  3. package/node_modules/@delexec/caller-controller/README.md +3 -0
  4. package/node_modules/@delexec/caller-controller/README.zh-CN.md +6 -0
  5. package/node_modules/@delexec/caller-controller/package.json +53 -0
  6. package/node_modules/@delexec/caller-controller/src/server.js +127 -0
  7. package/node_modules/@delexec/caller-controller-core/README.md +3 -0
  8. package/node_modules/@delexec/caller-controller-core/README.zh-CN.md +6 -0
  9. package/node_modules/@delexec/caller-controller-core/package.json +26 -0
  10. package/node_modules/@delexec/caller-controller-core/src/index.js +1612 -0
  11. package/node_modules/@delexec/caller-skill-adapter/package.json +12 -0
  12. package/node_modules/@delexec/caller-skill-adapter/src/server.js +1042 -0
  13. package/node_modules/@delexec/caller-skill-mcp-adapter/README.md +65 -0
  14. package/node_modules/@delexec/caller-skill-mcp-adapter/package.json +16 -0
  15. package/node_modules/@delexec/caller-skill-mcp-adapter/src/server.js +527 -0
  16. package/node_modules/@delexec/responder-controller/README.md +3 -0
  17. package/node_modules/@delexec/responder-controller/README.zh-CN.md +6 -0
  18. package/node_modules/@delexec/responder-controller/package.json +53 -0
  19. package/node_modules/@delexec/responder-controller/src/server.js +254 -0
  20. package/node_modules/@delexec/responder-runtime-core/README.md +3 -0
  21. package/node_modules/@delexec/responder-runtime-core/README.zh-CN.md +6 -0
  22. package/node_modules/@delexec/responder-runtime-core/package.json +26 -0
  23. package/node_modules/@delexec/responder-runtime-core/src/executors.js +326 -0
  24. package/node_modules/@delexec/responder-runtime-core/src/index.js +1202 -0
  25. package/node_modules/@delexec/runtime-utils/README.md +3 -0
  26. package/node_modules/@delexec/runtime-utils/README.zh-CN.md +6 -0
  27. package/node_modules/@delexec/runtime-utils/package.json +23 -0
  28. package/node_modules/@delexec/runtime-utils/src/index.js +338 -0
  29. package/node_modules/@delexec/sqlite-store/README.md +3 -0
  30. package/node_modules/@delexec/sqlite-store/README.zh-CN.md +6 -0
  31. package/node_modules/@delexec/sqlite-store/package.json +26 -0
  32. package/node_modules/@delexec/sqlite-store/src/index.js +68 -0
  33. package/node_modules/@delexec/transport-email/README.md +3 -0
  34. package/node_modules/@delexec/transport-email/README.zh-CN.md +6 -0
  35. package/node_modules/@delexec/transport-email/package.json +23 -0
  36. package/node_modules/@delexec/transport-email/src/index.js +185 -0
  37. package/node_modules/@delexec/transport-emailengine/README.md +3 -0
  38. package/node_modules/@delexec/transport-emailengine/README.zh-CN.md +6 -0
  39. package/node_modules/@delexec/transport-emailengine/package.json +26 -0
  40. package/node_modules/@delexec/transport-emailengine/src/index.js +210 -0
  41. package/node_modules/@delexec/transport-gmail/README.md +3 -0
  42. package/node_modules/@delexec/transport-gmail/README.zh-CN.md +6 -0
  43. package/node_modules/@delexec/transport-gmail/package.json +26 -0
  44. package/node_modules/@delexec/transport-gmail/src/index.js +295 -0
  45. package/node_modules/@delexec/transport-relay-http/README.md +3 -0
  46. package/node_modules/@delexec/transport-relay-http/README.zh-CN.md +6 -0
  47. package/node_modules/@delexec/transport-relay-http/package.json +23 -0
  48. package/node_modules/@delexec/transport-relay-http/src/index.js +124 -0
  49. package/package.json +64 -0
  50. package/src/cli.js +1571 -0
  51. package/src/config.js +1180 -0
  52. package/src/example-hotline-worker.js +65 -0
  53. package/src/example-hotline.js +196 -0
  54. package/src/logging.js +56 -0
  55. package/src/supervisor.js +3070 -0
@@ -0,0 +1,1612 @@
1
+ import crypto from "node:crypto";
2
+ import http from "node:http";
3
+
4
+ import { buildStructuredError, canonicalizeResultPackageForSignature } from "@delexec/contracts";
5
+
6
+ export const CALLER_TERMINAL_STATUSES = Object.freeze(["SUCCEEDED", "FAILED", "UNVERIFIED", "TIMED_OUT"]);
7
+ export const CALLER_ACTIVE_STATUSES = Object.freeze(["CREATED", "SENT", "ACKED"]);
8
+
9
+ const TERMINAL_STATUS_SET = new Set(CALLER_TERMINAL_STATUSES);
10
+ const ACTIVE_STATUS_SET = new Set(CALLER_ACTIVE_STATUSES);
11
+
12
+ function nowIso() {
13
+ return new Date().toISOString();
14
+ }
15
+
16
+ function parseJsonBody(req) {
17
+ return new Promise((resolve, reject) => {
18
+ const chunks = [];
19
+ req.on("data", (chunk) => chunks.push(chunk));
20
+ req.on("end", () => {
21
+ if (chunks.length === 0) {
22
+ resolve({});
23
+ return;
24
+ }
25
+ try {
26
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
27
+ } catch {
28
+ reject(new Error("invalid_json"));
29
+ }
30
+ });
31
+ req.on("error", reject);
32
+ });
33
+ }
34
+
35
+ function sendJson(res, statusCode, data) {
36
+ res.writeHead(statusCode, {
37
+ "content-type": "application/json; charset=utf-8",
38
+ "access-control-allow-origin": "*",
39
+ "access-control-allow-methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
40
+ "access-control-allow-headers": "Content-Type, Authorization, X-Platform-Api-Key"
41
+ });
42
+ res.end(JSON.stringify(data));
43
+ }
44
+
45
+ function sendError(res, statusCode, code, message, { retryable, ...extra } = {}) {
46
+ sendJson(res, statusCode, buildStructuredError(code, message, { retryable, ...extra }));
47
+ }
48
+
49
+ async function requestJson(baseUrl, pathname, { method = "GET", headers = {}, body } = {}) {
50
+ const response = await fetch(new URL(pathname, baseUrl), {
51
+ method,
52
+ headers: {
53
+ ...headers,
54
+ ...(body === undefined ? {} : { "content-type": "application/json; charset=utf-8" })
55
+ },
56
+ body: body === undefined ? undefined : JSON.stringify(body)
57
+ });
58
+
59
+ const text = await response.text();
60
+ return {
61
+ status: response.status,
62
+ headers: response.headers,
63
+ body: text ? JSON.parse(text) : null
64
+ };
65
+ }
66
+
67
+ function createUpstreamError(code, response) {
68
+ const error = new Error(code);
69
+ error.code = code;
70
+ error.response = response;
71
+ return error;
72
+ }
73
+
74
+ function normalizePemString(value) {
75
+ if (typeof value !== "string") {
76
+ return null;
77
+ }
78
+ const trimmed = value.trim();
79
+ if (!trimmed) {
80
+ return null;
81
+ }
82
+ return trimmed.replace(/\\n/g, "\n");
83
+ }
84
+
85
+ function isLocalOnlyRegistrationMode(value) {
86
+ return typeof value === "string" && value.trim().toLowerCase() === "local_only";
87
+ }
88
+
89
+ function sendUpstreamError(res, error, fallbackCode, fallbackMessage = "upstream service error") {
90
+ if (error?.response) {
91
+ sendJson(res, error.response.status, error.response.body || { error: { code: fallbackCode, message: fallbackMessage, retryable: true } });
92
+ return;
93
+ }
94
+
95
+ sendError(res, 502, fallbackCode, error instanceof Error ? error.message : fallbackMessage, { retryable: true });
96
+ }
97
+
98
+ export function loadCallerConfig() {
99
+ return {
100
+ ack_deadline_s: Number(process.env.ACK_DEADLINE_S || 120),
101
+ timeout_confirmation_mode: process.env.TIMEOUT_CONFIRMATION_MODE || "ask_by_default",
102
+ hard_timeout_auto_finalize: String(process.env.HARD_TIMEOUT_AUTO_FINALIZE || "true") === "true",
103
+ poll_interval_active_s: Number(process.env.CALLER_CONTROLLER_POLL_INTERVAL_ACTIVE_S || 5),
104
+ poll_interval_backoff_s: Number(process.env.CALLER_CONTROLLER_POLL_INTERVAL_BACKOFF_S || 15),
105
+ events_sync_batch_size: Number(process.env.CALLER_CONTROLLER_EVENTS_SYNC_BATCH_SIZE || 25)
106
+ };
107
+ }
108
+
109
+ export function createCallerState() {
110
+ return { requests: new Map() };
111
+ }
112
+
113
+ export function serializeCallerState(state) {
114
+ return {
115
+ requests: Array.from(state.requests.entries())
116
+ };
117
+ }
118
+
119
+ export function hydrateCallerState(state, snapshot) {
120
+ if (!snapshot) {
121
+ return state;
122
+ }
123
+
124
+ state.requests.clear();
125
+ for (const [requestId, request] of snapshot.requests || []) {
126
+ state.requests.set(requestId, request);
127
+ }
128
+ return state;
129
+ }
130
+
131
+ export function createCallerPlatformClient({ baseUrl, apiKey } = {}) {
132
+ if (!baseUrl) {
133
+ throw new Error("caller_platform_base_url_required");
134
+ }
135
+
136
+ function authHeaders(required = false) {
137
+ if (!apiKey) {
138
+ if (required) {
139
+ throw new Error("caller_platform_api_key_required");
140
+ }
141
+ return {};
142
+ }
143
+
144
+ return {
145
+ Authorization: `Bearer ${apiKey}`
146
+ };
147
+ }
148
+
149
+ return {
150
+ config: {
151
+ baseUrl,
152
+ apiKey: apiKey || null
153
+ },
154
+
155
+ async registerCaller({ contactEmail, contact_email, email } = {}) {
156
+ const response = await requestJson(baseUrl, "/v1/users/register", {
157
+ method: "POST",
158
+ body: {
159
+ ...(contactEmail || contact_email ? { contact_email: contactEmail || contact_email } : {}),
160
+ ...(email ? { email } : {})
161
+ }
162
+ });
163
+
164
+ if (response.status !== 201) {
165
+ throw createUpstreamError("CALLER_PLATFORM_REGISTER_FAILED", response);
166
+ }
167
+
168
+ return response.body;
169
+ },
170
+
171
+ async listCatalogHotlines(filters = {}) {
172
+ const params = new URLSearchParams();
173
+ if (filters.status) {
174
+ params.set("status", filters.status);
175
+ }
176
+ if (filters.availability_status) {
177
+ params.set("availability_status", filters.availability_status);
178
+ }
179
+ if (filters.task_type) {
180
+ params.set("task_type", filters.task_type);
181
+ }
182
+ if (filters.capability) {
183
+ params.set("capability", filters.capability);
184
+ }
185
+ if (filters.tag) {
186
+ params.set("tag", filters.tag);
187
+ }
188
+
189
+ const pathname = `/v2/hotlines${params.size > 0 ? `?${params.toString()}` : ""}`;
190
+ const response = await requestJson(baseUrl, pathname, {
191
+ headers: authHeaders(false)
192
+ });
193
+ if (response.status !== 200) {
194
+ throw createUpstreamError("CALLER_PLATFORM_CATALOG_FAILED", response);
195
+ }
196
+
197
+ let items = response.body?.items || [];
198
+ if (filters.responder_id) {
199
+ items = items.filter((item) => item.responder_id === filters.responder_id);
200
+ }
201
+ if (filters.hotline_id) {
202
+ items = items.filter((item) => item.hotline_id === filters.hotline_id);
203
+ }
204
+
205
+ return {
206
+ ...response.body,
207
+ items
208
+ };
209
+ },
210
+
211
+ async registerResponder(body = {}) {
212
+ const response = await requestJson(baseUrl, "/v2/responders/register", {
213
+ method: "POST",
214
+ headers: authHeaders(true),
215
+ body
216
+ });
217
+
218
+ if (response.status !== 201) {
219
+ throw createUpstreamError("CALLER_PLATFORM_RESPONDER_REGISTER_FAILED", response);
220
+ }
221
+
222
+ return response.body;
223
+ },
224
+
225
+ async issueTaskToken({ requestId, responderId, hotlineId }) {
226
+ const response = await requestJson(baseUrl, "/v1/tokens/task", {
227
+ method: "POST",
228
+ headers: authHeaders(true),
229
+ body: {
230
+ request_id: requestId,
231
+ responder_id: responderId,
232
+ hotline_id: hotlineId
233
+ }
234
+ });
235
+
236
+ if (response.status !== 201) {
237
+ throw createUpstreamError("CALLER_PLATFORM_TOKEN_FAILED", response);
238
+ }
239
+
240
+ return response.body;
241
+ },
242
+
243
+ async getDeliveryMeta({ requestId, responderId, hotlineId, taskToken, resultDelivery }) {
244
+ const response = await requestJson(baseUrl, `/v1/requests/${requestId}/delivery-meta`, {
245
+ method: "POST",
246
+ headers: authHeaders(true),
247
+ body: {
248
+ responder_id: responderId,
249
+ hotline_id: hotlineId,
250
+ task_token: taskToken,
251
+ result_delivery: resultDelivery
252
+ }
253
+ });
254
+
255
+ if (response.status !== 200) {
256
+ throw createUpstreamError("CALLER_PLATFORM_DELIVERY_META_FAILED", response);
257
+ }
258
+
259
+ return response.body;
260
+ },
261
+
262
+ async getRequestEvents(requestId) {
263
+ const response = await requestJson(baseUrl, `/v1/requests/${requestId}/events`, {
264
+ headers: authHeaders(true)
265
+ });
266
+
267
+ if (response.status !== 200) {
268
+ throw createUpstreamError("CALLER_PLATFORM_EVENTS_FAILED", response);
269
+ }
270
+
271
+ return response.body;
272
+ },
273
+
274
+ async getRequestEventsBatch(requestIds = []) {
275
+ const response = await requestJson(baseUrl, "/v1/requests/events/batch", {
276
+ method: "POST",
277
+ headers: authHeaders(true),
278
+ body: {
279
+ request_ids: Array.isArray(requestIds) ? requestIds : []
280
+ }
281
+ });
282
+
283
+ if (response.status !== 200) {
284
+ throw createUpstreamError("CALLER_PLATFORM_EVENTS_BATCH_FAILED", response);
285
+ }
286
+
287
+ return response.body;
288
+ },
289
+
290
+ async postMetricEvent(body) {
291
+ const response = await requestJson(baseUrl, "/v1/metrics/events", {
292
+ method: "POST",
293
+ headers: authHeaders(true),
294
+ body
295
+ });
296
+
297
+ if (response.status !== 202) {
298
+ throw createUpstreamError("CALLER_PLATFORM_METRIC_FAILED", response);
299
+ }
300
+
301
+ return response.body;
302
+ }
303
+ };
304
+ }
305
+
306
+ function createLocalFallbackPlatformClient() {
307
+ const responderId = process.env.RESPONDER_ID || null;
308
+ const responderPublicKeyPem = normalizePemString(process.env.RESPONDER_SIGNING_PUBLIC_KEY_PEM);
309
+ const hotlineIds = String(process.env.HOTLINE_IDS || "")
310
+ .split(",")
311
+ .map((value) => value.trim())
312
+ .filter(Boolean);
313
+
314
+ if (!responderId || !responderPublicKeyPem || hotlineIds.length === 0) {
315
+ return null;
316
+ }
317
+
318
+ return {
319
+ config: {
320
+ baseUrl: "local://responder",
321
+ apiKey: null
322
+ },
323
+
324
+ async listCatalogHotlines(filters = {}) {
325
+ let items = hotlineIds.map((hotlineId) => ({
326
+ responder_id: responderId,
327
+ hotline_id: hotlineId,
328
+ display_name: hotlineId,
329
+ availability_status: "healthy",
330
+ responder_public_key_pem: responderPublicKeyPem,
331
+ task_types: [],
332
+ capabilities: [],
333
+ tags: [],
334
+ review_status: "local_only",
335
+ catalog_visibility: "local"
336
+ }));
337
+ if (filters.responder_id) {
338
+ items = items.filter((item) => item.responder_id === filters.responder_id);
339
+ }
340
+ if (filters.hotline_id) {
341
+ items = items.filter((item) => item.hotline_id === filters.hotline_id);
342
+ }
343
+ return { items };
344
+ },
345
+
346
+ async issueTaskToken({ requestId, responderId: nextResponderId, hotlineId }) {
347
+ if (nextResponderId !== responderId || !hotlineIds.includes(hotlineId)) {
348
+ const error = new Error("CATALOG_HOTLINE_NOT_FOUND");
349
+ error.response = {
350
+ status: 404,
351
+ body: buildStructuredError("CATALOG_HOTLINE_NOT_FOUND", "hotline not found or not enabled")
352
+ };
353
+ throw error;
354
+ }
355
+ return {
356
+ task_token: `local_task_${requestId}`,
357
+ claims: {
358
+ mode: "local",
359
+ request_id: requestId,
360
+ responder_id: nextResponderId,
361
+ hotline_id: hotlineId
362
+ }
363
+ };
364
+ },
365
+
366
+ async getDeliveryMeta({ responderId: nextResponderId, hotlineId, taskToken, resultDelivery }) {
367
+ if (nextResponderId !== responderId || !hotlineIds.includes(hotlineId)) {
368
+ const error = new Error("CATALOG_HOTLINE_NOT_FOUND");
369
+ error.response = {
370
+ status: 404,
371
+ body: buildStructuredError("CATALOG_HOTLINE_NOT_FOUND", "hotline not found or not enabled")
372
+ };
373
+ throw error;
374
+ }
375
+ return {
376
+ task_delivery: {
377
+ kind: "local",
378
+ address: responderId,
379
+ receiver: responderId
380
+ },
381
+ result_delivery: resultDelivery || { kind: "local", address: "caller-controller" },
382
+ verification: {
383
+ display_code: crypto.randomBytes(3).toString("hex").toUpperCase()
384
+ },
385
+ responder_public_key_pem: responderPublicKeyPem,
386
+ task_token: taskToken
387
+ };
388
+ },
389
+
390
+ async postMetricEvent() {
391
+ return { accepted: true, mode: "local" };
392
+ }
393
+ };
394
+ }
395
+
396
+ export function evaluateTimeouts(request, config) {
397
+ if (TERMINAL_STATUS_SET.has(request.status)) {
398
+ return null;
399
+ }
400
+
401
+ const now = Date.now();
402
+ const ackDeadlineAt = request.ack_deadline_at ? new Date(request.ack_deadline_at).getTime() : null;
403
+ const softTimeoutAt = new Date(request.soft_timeout_at).getTime();
404
+ const hardTimeoutAt = new Date(request.hard_timeout_at).getTime();
405
+
406
+ if (
407
+ request.status === "SENT" &&
408
+ ackDeadlineAt &&
409
+ now >= ackDeadlineAt &&
410
+ !request.acknowledged_at &&
411
+ request.timeout_decision !== "continue_wait"
412
+ ) {
413
+ request.status = "TIMED_OUT";
414
+ request.timed_out_at = nowIso();
415
+ request.last_error_code = "DELIVERY_OR_ACCEPTANCE_TIMEOUT";
416
+ request.needs_timeout_confirmation = false;
417
+ return {
418
+ status: request.status,
419
+ eventType: "caller.request.timed_out",
420
+ code: request.last_error_code
421
+ };
422
+ }
423
+
424
+ if (
425
+ config.timeout_confirmation_mode === "ask_by_default" &&
426
+ now >= softTimeoutAt &&
427
+ request.timeout_decision === "pending"
428
+ ) {
429
+ request.needs_timeout_confirmation = true;
430
+ }
431
+
432
+ if (
433
+ config.hard_timeout_auto_finalize &&
434
+ ACTIVE_STATUS_SET.has(request.status) &&
435
+ now >= hardTimeoutAt &&
436
+ request.timeout_decision !== "continue_wait"
437
+ ) {
438
+ request.status = "TIMED_OUT";
439
+ request.timed_out_at = nowIso();
440
+ request.last_error_code = "EXEC_TIMEOUT_HARD";
441
+ request.needs_timeout_confirmation = false;
442
+ return {
443
+ status: request.status,
444
+ eventType: "caller.request.timed_out",
445
+ code: request.last_error_code
446
+ };
447
+ }
448
+
449
+ return null;
450
+ }
451
+
452
+ export function createRequestRecord(config, body) {
453
+ const requestId = body.request_id || `req_${crypto.randomUUID()}`;
454
+ const ackDeadlineS = Number(body.ack_deadline_s || config.ack_deadline_s || 120);
455
+ const softTimeoutS = Number(body.soft_timeout_s || 90);
456
+ const hardTimeoutS = Number(body.hard_timeout_s || 300);
457
+ const createdAtMs = Date.now();
458
+
459
+ return {
460
+ request_id: requestId,
461
+ caller_id: body.caller_id || "caller_default",
462
+ responder_id: body.responder_id || null,
463
+ hotline_id: body.hotline_id || null,
464
+ contract_version: body.contract_version || "0.1.0",
465
+ expected_signer_public_key_pem: body.expected_signer_public_key_pem || null,
466
+ status: "CREATED",
467
+ attempt: Number(body.attempt || 1),
468
+ timeout_decision: "pending",
469
+ needs_timeout_confirmation: false,
470
+ timeline: [{ at: nowIso(), event: "CREATED" }],
471
+ created_at: new Date(createdAtMs).toISOString(),
472
+ updated_at: new Date(createdAtMs).toISOString(),
473
+ ack_deadline_s: ackDeadlineS,
474
+ ack_deadline_at: body.ack_deadline_at || null,
475
+ soft_timeout_s: softTimeoutS,
476
+ hard_timeout_s: hardTimeoutS,
477
+ soft_timeout_at: new Date(createdAtMs + softTimeoutS * 1000).toISOString(),
478
+ hard_timeout_at: new Date(createdAtMs + hardTimeoutS * 1000).toISOString(),
479
+ config_snapshot: {
480
+ ack_deadline_s: ackDeadlineS,
481
+ timeout_confirmation_mode: config.timeout_confirmation_mode,
482
+ hard_timeout_auto_finalize: config.hard_timeout_auto_finalize
483
+ },
484
+ task_token: body.task_token || null,
485
+ result_delivery: body.result_delivery || { kind: "local", address: "caller-controller" },
486
+ verification: body.verification || null,
487
+ delivery_meta: body.delivery_meta || null,
488
+ platform_events: [],
489
+ platform_completed_at: null,
490
+ platform_failed_at: null,
491
+ platform_last_event: null,
492
+ result_package: null,
493
+ contract_draft: body.contract_draft || null,
494
+ last_error_code: null,
495
+ metric_flags: {}
496
+ };
497
+ }
498
+
499
+ function getTaskDelivery(request, body = {}) {
500
+ return request.delivery_meta?.task_delivery || body.task_delivery || null;
501
+ }
502
+
503
+ function getResultDelivery(request, body = {}) {
504
+ return request.delivery_meta?.result_delivery || body.result_delivery || request.result_delivery || null;
505
+ }
506
+
507
+ function extractEmailResult(envelope) {
508
+ if (!envelope || typeof envelope !== "object") {
509
+ return { resultPackage: null, attachments: [], parseError: false };
510
+ }
511
+
512
+ if (envelope.result_package || envelope.payload?.result_package) {
513
+ return {
514
+ resultPackage: envelope.result_package || envelope.payload?.result_package,
515
+ attachments: envelope.attachments || envelope.payload?.attachments || [],
516
+ parseError: false
517
+ };
518
+ }
519
+
520
+ if (typeof envelope.body_text === "string" && envelope.body_text.trim()) {
521
+ try {
522
+ return {
523
+ resultPackage: JSON.parse(envelope.body_text),
524
+ attachments: envelope.attachments || [],
525
+ parseError: false
526
+ };
527
+ } catch {
528
+ return { resultPackage: null, attachments: envelope.attachments || [], parseError: true };
529
+ }
530
+ }
531
+
532
+ return {
533
+ resultPackage: envelope.payload?.request_id ? envelope.payload : null,
534
+ attachments: envelope.attachments || [],
535
+ parseError: false
536
+ };
537
+ }
538
+
539
+ function verifyArtifactBindings(body, attachments = []) {
540
+ const declared = Array.isArray(body.artifacts) ? body.artifacts : [];
541
+ if (declared.length === 0) {
542
+ return attachments.length === 0;
543
+ }
544
+
545
+ for (const artifact of declared) {
546
+ const attachment = attachments.find((item) => item.name === artifact.name);
547
+ if (!attachment) {
548
+ return false;
549
+ }
550
+ if (artifact.media_type && attachment.media_type !== artifact.media_type) {
551
+ return false;
552
+ }
553
+ if (Number.isFinite(Number(artifact.byte_size)) && Number(artifact.byte_size) !== Number(attachment.byte_size)) {
554
+ return false;
555
+ }
556
+ if (artifact.sha256) {
557
+ const digest = crypto.createHash("sha256").update(Buffer.from(attachment.content_base64 || "", "base64")).digest("hex");
558
+ if (digest !== artifact.sha256) {
559
+ return false;
560
+ }
561
+ }
562
+ }
563
+
564
+ return true;
565
+ }
566
+
567
+ function markUpdated(request, event) {
568
+ request.updated_at = nowIso();
569
+ request.timeline.push({ at: request.updated_at, event });
570
+ }
571
+
572
+ function setSentState(request) {
573
+ request.status = "SENT";
574
+ request.last_error_code = null;
575
+ if (!request.sent_at) {
576
+ request.sent_at = nowIso();
577
+ }
578
+ request.ack_deadline_at = new Date(Date.now() + request.ack_deadline_s * 1000).toISOString();
579
+ markUpdated(request, "SENT");
580
+ }
581
+
582
+ function resultContextMatchesRequest(request, body) {
583
+ if (body.request_id !== request.request_id) {
584
+ return false;
585
+ }
586
+
587
+ if (typeof body.result_version === "string" && body.result_version !== "0.1.0") {
588
+ return false;
589
+ }
590
+
591
+ if (request.responder_id && body.responder_id !== request.responder_id) {
592
+ return false;
593
+ }
594
+
595
+ if (request.hotline_id && body.hotline_id !== request.hotline_id) {
596
+ return false;
597
+ }
598
+
599
+ if (request.verification?.display_code && body.verification?.display_code !== request.verification.display_code) {
600
+ return false;
601
+ }
602
+
603
+ return true;
604
+ }
605
+
606
+ function verifyResultSignature(request, body) {
607
+ if (!body.signature_base64) {
608
+ return body.signature_valid !== false;
609
+ }
610
+
611
+ if (!request.expected_signer_public_key_pem) {
612
+ return false;
613
+ }
614
+
615
+ try {
616
+ const bytes = Buffer.from(JSON.stringify(canonicalizeResultPackageForSignature(body)), "utf8");
617
+ const signature = Buffer.from(body.signature_base64, "base64");
618
+ const publicKey = crypto.createPublicKey(normalizePemString(request.expected_signer_public_key_pem));
619
+ return crypto.verify(null, bytes, publicKey, signature);
620
+ } catch {
621
+ return false;
622
+ }
623
+ }
624
+
625
+ export function applyResultPackage(request, body, { attachments = [] } = {}) {
626
+ request.result_package = body;
627
+
628
+ if (!resultContextMatchesRequest(request, body)) {
629
+ request.status = "UNVERIFIED";
630
+ request.last_error_code = "RESULT_CONTEXT_MISMATCH";
631
+ markUpdated(request, "RESULT_CONTEXT_MISMATCH");
632
+ return {
633
+ status: request.status,
634
+ eventType: "caller.request.unverified",
635
+ code: request.last_error_code
636
+ };
637
+ }
638
+
639
+ if (!verifyResultSignature(request, body)) {
640
+ request.status = "UNVERIFIED";
641
+ request.last_error_code = "RESULT_SIGNATURE_INVALID";
642
+ markUpdated(request, "RESULT_SIGNATURE_INVALID");
643
+ return {
644
+ status: request.status,
645
+ eventType: "caller.request.unverified",
646
+ code: request.last_error_code
647
+ };
648
+ }
649
+
650
+ if (!verifyArtifactBindings(body, attachments)) {
651
+ request.status = "UNVERIFIED";
652
+ request.last_error_code = "RESULT_ARTIFACT_INVALID";
653
+ markUpdated(request, "RESULT_ARTIFACT_INVALID");
654
+ return {
655
+ status: request.status,
656
+ eventType: "caller.request.unverified",
657
+ code: request.last_error_code
658
+ };
659
+ }
660
+
661
+ if (body.schema_valid === false) {
662
+ request.status = "UNVERIFIED";
663
+ request.last_error_code = "RESULT_SCHEMA_INVALID";
664
+ markUpdated(request, "RESULT_SCHEMA_INVALID");
665
+ return {
666
+ status: request.status,
667
+ eventType: "caller.request.unverified",
668
+ code: request.last_error_code
669
+ };
670
+ }
671
+
672
+ if (body.status === "ok") {
673
+ request.status = "SUCCEEDED";
674
+ request.last_error_code = null;
675
+ markUpdated(request, "SUCCEEDED");
676
+ return {
677
+ status: request.status,
678
+ eventType: "caller.request.succeeded"
679
+ };
680
+ }
681
+
682
+ request.status = "FAILED";
683
+ request.last_error_code = body.error?.code || "EXEC_UNKNOWN";
684
+ markUpdated(request, "FAILED");
685
+ return {
686
+ status: request.status,
687
+ eventType: "caller.request.failed",
688
+ code: request.last_error_code
689
+ };
690
+ }
691
+
692
+ async function reportCallerMetric(platformClient, request, eventType, detail = {}) {
693
+ if (!platformClient || !eventType) {
694
+ return;
695
+ }
696
+
697
+ const metricKey = `${eventType}:${detail.code || ""}`;
698
+ request.metric_flags ||= {};
699
+ if (request.metric_flags[metricKey]) {
700
+ return;
701
+ }
702
+
703
+ request.metric_flags[metricKey] = true;
704
+ try {
705
+ await platformClient.postMetricEvent({
706
+ source: "caller-controller",
707
+ event_type: eventType,
708
+ request_id: request.request_id,
709
+ responder_id: request.responder_id,
710
+ hotline_id: request.hotline_id,
711
+ ...detail
712
+ });
713
+ } catch (error) {
714
+ request.last_metric_error = error instanceof Error ? error.message : "metric_event_failed";
715
+ }
716
+ }
717
+
718
+ async function evaluateTimeoutsWithMetrics(request, config, platformClient) {
719
+ const transition = evaluateTimeouts(request, config);
720
+ if (transition?.eventType) {
721
+ await reportCallerMetric(platformClient, request, transition.eventType, { code: transition.code });
722
+ }
723
+ return transition;
724
+ }
725
+
726
+ async function persistCallerState(onStateChanged, state) {
727
+ if (typeof onStateChanged === "function") {
728
+ await onStateChanged(state);
729
+ }
730
+ }
731
+
732
+ function isCallerRequestActive(request) {
733
+ return ACTIVE_STATUS_SET.has(request.status);
734
+ }
735
+
736
+ async function pollCallerInbox(
737
+ state,
738
+ transport,
739
+ platformClientResolver,
740
+ onStateChanged,
741
+ receiver = "caller-controller"
742
+ ) {
743
+ if (!transport) {
744
+ return { accepted: [], mutated: false };
745
+ }
746
+
747
+ const polled = await transport.poll({
748
+ limit: 10,
749
+ receiver
750
+ });
751
+ const accepted = [];
752
+ let mutated = false;
753
+
754
+ for (const envelope of polled.items) {
755
+ const { resultPackage, attachments, parseError } = extractEmailResult(envelope);
756
+ if (!resultPackage?.request_id && parseError && envelope.request_id) {
757
+ const request = state.requests.get(envelope.request_id);
758
+ if (request && !TERMINAL_STATUS_SET.has(request.status)) {
759
+ request.status = "UNVERIFIED";
760
+ request.last_error_code = "RESULT_BODY_INVALID_JSON";
761
+ markUpdated(request, "RESULT_BODY_INVALID_JSON");
762
+ mutated = true;
763
+ }
764
+ await transport.ack(envelope.message_id, { receiver });
765
+ accepted.push({ message_id: envelope.message_id, request_id: envelope.request_id });
766
+ continue;
767
+ }
768
+ if (!resultPackage?.request_id) {
769
+ continue;
770
+ }
771
+
772
+ const request = state.requests.get(resultPackage.request_id);
773
+ if (!request) {
774
+ continue;
775
+ }
776
+ const platformClient = typeof platformClientResolver === "function" ? platformClientResolver(request) : null;
777
+
778
+ if (!TERMINAL_STATUS_SET.has(request.status)) {
779
+ const transition = applyResultPackage(request, resultPackage, { attachments });
780
+ if (transition?.eventType) {
781
+ await reportCallerMetric(platformClient, request, transition.eventType, { code: transition.code });
782
+ }
783
+ mutated = true;
784
+ }
785
+
786
+ await transport.ack(envelope.message_id, { receiver });
787
+ accepted.push({ message_id: envelope.message_id, request_id: resultPackage.request_id });
788
+ }
789
+
790
+ if (mutated) {
791
+ await persistCallerState(onStateChanged, state);
792
+ }
793
+
794
+ return { accepted, mutated };
795
+ }
796
+
797
+ async function syncCallerActiveRequests(state, config, platformClientFactory, onStateChanged) {
798
+ let mutated = false;
799
+ const activeGroups = new Map();
800
+ const requestClients = new Map();
801
+
802
+ for (const request of state.requests.values()) {
803
+ const platformClient = typeof platformClientFactory === "function" ? platformClientFactory(request) : null;
804
+ requestClients.set(request.request_id, platformClient);
805
+
806
+ if (!platformClient || !isCallerRequestActive(request)) {
807
+ continue;
808
+ }
809
+
810
+ const clientKey = `${platformClient.config?.baseUrl || "unknown"}::${platformClient.config?.apiKey || "anonymous"}`;
811
+ const existingGroup = activeGroups.get(clientKey) || {
812
+ client: platformClient,
813
+ requests: []
814
+ };
815
+ existingGroup.requests.push(request);
816
+ activeGroups.set(clientKey, existingGroup);
817
+ }
818
+
819
+ const batchSize = Math.max(1, Number(config.events_sync_batch_size || 25));
820
+
821
+ for (const group of activeGroups.values()) {
822
+ const { client: platformClient, requests } = group;
823
+ for (let index = 0; index < requests.length; index += batchSize) {
824
+ const batch = requests.slice(index, index + batchSize);
825
+ const requestIds = batch.map((request) => request.request_id);
826
+ try {
827
+ const response = await platformClient.getRequestEventsBatch(requestIds);
828
+ const byRequestId = new Map((response.items || []).map((item) => [item.request_id, item]));
829
+ for (const request of batch) {
830
+ const item = byRequestId.get(request.request_id);
831
+ if (!item || item.found === false) {
832
+ continue;
833
+ }
834
+ const synced = applyPlatformEventsToRequest(request, item.events || item.items || []);
835
+ if (synced.acked) {
836
+ await reportCallerMetric(platformClient, request, "caller.request.acked");
837
+ }
838
+ mutated = true;
839
+ }
840
+ } catch {
841
+ for (const request of batch) {
842
+ try {
843
+ const synced = await syncCallerRequestEvents(request, platformClient);
844
+ if (synced.acked) {
845
+ await reportCallerMetric(platformClient, request, "caller.request.acked");
846
+ }
847
+ mutated = true;
848
+ } catch {
849
+ // ignore background sync failures; foreground APIs still expose explicit sync
850
+ }
851
+ }
852
+ }
853
+ }
854
+ }
855
+
856
+ for (const request of state.requests.values()) {
857
+ const transition = await evaluateTimeoutsWithMetrics(
858
+ request,
859
+ config,
860
+ requestClients.get(request.request_id) || null
861
+ );
862
+ mutated ||= Boolean(transition);
863
+ }
864
+
865
+ if (mutated) {
866
+ await persistCallerState(onStateChanged, state);
867
+ }
868
+
869
+ return { mutated };
870
+ }
871
+
872
+ export function startCallerBackgroundLoops({
873
+ state,
874
+ config = loadCallerConfig(),
875
+ transport = null,
876
+ receiver = "caller-controller",
877
+ inboxPollIntervalMs = 1000,
878
+ eventsSyncIntervalMs = 1000,
879
+ platformClientFactory = () => null,
880
+ onStateChanged = null,
881
+ logger = console
882
+ } = {}) {
883
+ let stopped = false;
884
+ let inboxRunning = false;
885
+ let syncRunning = false;
886
+
887
+ async function runInboxPoll() {
888
+ if (stopped || inboxRunning || !transport) {
889
+ return;
890
+ }
891
+ inboxRunning = true;
892
+ try {
893
+ await pollCallerInbox(state, transport, platformClientFactory, onStateChanged, receiver);
894
+ } catch (error) {
895
+ logger?.warn?.(`[caller-background] inbox poll failed: ${error instanceof Error ? error.message : "unknown_error"}`);
896
+ } finally {
897
+ inboxRunning = false;
898
+ }
899
+ }
900
+
901
+ async function runEventSync() {
902
+ if (stopped || syncRunning) {
903
+ return;
904
+ }
905
+ syncRunning = true;
906
+ try {
907
+ await syncCallerActiveRequests(state, config, platformClientFactory, onStateChanged);
908
+ } catch (error) {
909
+ logger?.warn?.(`[caller-background] request sync failed: ${error instanceof Error ? error.message : "unknown_error"}`);
910
+ } finally {
911
+ syncRunning = false;
912
+ }
913
+ }
914
+
915
+ void runInboxPoll();
916
+ void runEventSync();
917
+
918
+ const inboxTimer = transport
919
+ ? setInterval(() => {
920
+ void runInboxPoll();
921
+ }, inboxPollIntervalMs)
922
+ : null;
923
+ const syncTimer = setInterval(() => {
924
+ void runEventSync();
925
+ }, eventsSyncIntervalMs);
926
+
927
+ return () => {
928
+ stopped = true;
929
+ if (inboxTimer) {
930
+ clearInterval(inboxTimer);
931
+ }
932
+ clearInterval(syncTimer);
933
+ };
934
+ }
935
+
936
+ export async function prepareCallerRequest(request, platformClient, options = {}) {
937
+ const responderId = options.responder_id || options.responderId || request.responder_id;
938
+ const hotlineId = options.hotline_id || options.hotlineId || request.hotline_id;
939
+ if (!responderId || !hotlineId) {
940
+ throw new Error("caller_prepare_requires_responder_and_hotline");
941
+ }
942
+
943
+ const issued = await platformClient.issueTaskToken({
944
+ requestId: request.request_id,
945
+ responderId,
946
+ hotlineId
947
+ });
948
+ const deliveryMeta = await platformClient.getDeliveryMeta({
949
+ requestId: request.request_id,
950
+ responderId,
951
+ hotlineId,
952
+ taskToken: issued.task_token,
953
+ resultDelivery: options.result_delivery || options.resultDelivery || request.result_delivery
954
+ });
955
+
956
+ const expectedSignerPublicKeyPem = normalizePemString(request.expected_signer_public_key_pem);
957
+ const deliveredSignerPublicKeyPem = normalizePemString(deliveryMeta.responder_public_key_pem);
958
+
959
+ if (expectedSignerPublicKeyPem && deliveredSignerPublicKeyPem && expectedSignerPublicKeyPem !== deliveredSignerPublicKeyPem) {
960
+ throw new Error("caller_signer_binding_mismatch");
961
+ }
962
+
963
+ request.responder_id = responderId;
964
+ request.hotline_id = hotlineId;
965
+ request.task_token = issued.task_token;
966
+ request.result_delivery = deliveryMeta.result_delivery || request.result_delivery || null;
967
+ request.verification = deliveryMeta.verification || request.verification || null;
968
+ request.delivery_meta = deliveryMeta;
969
+ request.expected_signer_public_key_pem = expectedSignerPublicKeyPem || deliveredSignerPublicKeyPem || null;
970
+ request.last_error_code = null;
971
+ markUpdated(request, "PREPARED");
972
+
973
+ return {
974
+ task_token: issued.task_token,
975
+ claims: issued.claims,
976
+ delivery_meta: deliveryMeta,
977
+ request
978
+ };
979
+ }
980
+
981
+ export function buildDispatchEnvelope(request, body = {}) {
982
+ const taskDelivery = getTaskDelivery(request, body);
983
+ const resultDelivery = getResultDelivery(request, body);
984
+ const deliveryAddress = taskDelivery?.address || body.task_delivery_address || request.responder_id;
985
+ const threadHint = taskDelivery?.thread_hint || request.delivery_meta?.thread_hint || `req:${request.request_id}`;
986
+
987
+ return {
988
+ message_id: body.message_id || `msg_${crypto.randomUUID()}`,
989
+ thread_id: body.thread_id || threadHint,
990
+ from: body.from || "caller-controller",
991
+ to: body.to || deliveryAddress,
992
+ type: body.type || "task.requested",
993
+ request_id: request.request_id,
994
+ responder_id: request.responder_id,
995
+ hotline_id: request.hotline_id,
996
+ task_token: body.task_token || request.task_token || null,
997
+ result_delivery: resultDelivery,
998
+ verification: request.verification || request.delivery_meta?.verification || null,
999
+ payload: body.payload || {},
1000
+ simulate: body.simulate || "success",
1001
+ delay_ms: Number(body.delay_ms || 80),
1002
+ lease_ttl_s: Number(body.lease_ttl_s || 30),
1003
+ priority: Number(body.priority || 5),
1004
+ sent_at: nowIso()
1005
+ };
1006
+ }
1007
+
1008
+ export function createTaskContractDraft(request, body = {}) {
1009
+ const taskInput = body.task_input ?? body.input ?? request.task_input ?? {};
1010
+ const outputSchema = body.output_schema ?? request.output_schema ?? null;
1011
+ const taskType = body.task_type || request.task_type || null;
1012
+ const resultDelivery = body.result_delivery || getResultDelivery(request, body);
1013
+ const threadHint = body.thread_hint || getTaskDelivery(request, body)?.thread_hint || `req:${request.request_id}`;
1014
+ const sourceRunId = body.source_run_id || request.source_run_id || null;
1015
+ const createdAt = body.created_at || nowIso();
1016
+ const constraints = {
1017
+ soft_timeout_s: body.soft_timeout_s ?? request.soft_timeout_s ?? null,
1018
+ hard_timeout_s: body.hard_timeout_s ?? request.hard_timeout_s ?? null
1019
+ };
1020
+
1021
+ const contract = {
1022
+ request_id: request.request_id,
1023
+ contract_version: body.contract_version || request.contract_version || "0.1.0",
1024
+ created_at: createdAt,
1025
+ caller: {
1026
+ caller_id: body.caller_id || request.caller_id || "caller_default"
1027
+ },
1028
+ responder: {
1029
+ responder_id: body.responder_id || request.responder_id,
1030
+ hotline_id: body.hotline_id || request.hotline_id
1031
+ },
1032
+ task: {
1033
+ task_type: taskType,
1034
+ input: taskInput,
1035
+ output_schema: outputSchema
1036
+ },
1037
+ constraints,
1038
+ token: body.task_token || request.task_token || null,
1039
+ trace: {
1040
+ thread_hint: threadHint
1041
+ }
1042
+ };
1043
+
1044
+ if (resultDelivery) {
1045
+ contract.caller.result_delivery = resultDelivery;
1046
+ }
1047
+ if (request.verification || body.verification) {
1048
+ contract.verification = body.verification || request.verification;
1049
+ }
1050
+ if (sourceRunId) {
1051
+ contract.trace.source_run_id = sourceRunId;
1052
+ }
1053
+
1054
+ request.task_type = taskType;
1055
+ request.task_input = taskInput;
1056
+ request.output_schema = outputSchema;
1057
+ request.result_delivery = resultDelivery;
1058
+ request.source_run_id = sourceRunId;
1059
+ request.contract_draft = contract;
1060
+ markUpdated(request, "CONTRACT_DRAFTED");
1061
+
1062
+ return contract;
1063
+ }
1064
+
1065
+ export async function syncCallerRequestEvents(request, platformClient) {
1066
+ const response = await platformClient.getRequestEvents(request.request_id);
1067
+ return applyPlatformEventsToRequest(request, response.events || response.items || []);
1068
+ }
1069
+
1070
+ export function applyPlatformEventsToRequest(request, events = []) {
1071
+ request.platform_events = events;
1072
+ request.platform_last_event = events.length > 0 ? events[events.length - 1] : null;
1073
+
1074
+ const ackEvent = events.find((event) => event.event_type === "ACKED");
1075
+ const completedEvent = events.find((event) => event.event_type === "COMPLETED");
1076
+ const failedEvent = events.find((event) => event.event_type === "FAILED");
1077
+ request.platform_completed_at = completedEvent?.finished_at || completedEvent?.at || null;
1078
+ request.platform_failed_at = failedEvent?.finished_at || failedEvent?.at || null;
1079
+ if (ackEvent && !TERMINAL_STATUS_SET.has(request.status) && request.status !== "ACKED") {
1080
+ request.status = "ACKED";
1081
+ request.last_error_code = null;
1082
+ request.acknowledged_at = ackEvent.at || null;
1083
+ request.ack_eta_hint_s = Number.isFinite(Number(ackEvent.eta_hint_s)) ? Number(ackEvent.eta_hint_s) : null;
1084
+ markUpdated(request, "ACKED");
1085
+ }
1086
+
1087
+ return {
1088
+ request_id: request.request_id,
1089
+ events,
1090
+ acked: Boolean(ackEvent),
1091
+ request
1092
+ };
1093
+ }
1094
+
1095
+ export function createCallerControllerServer({
1096
+ state = createCallerState(),
1097
+ serviceName = "caller-controller",
1098
+ config = loadCallerConfig(),
1099
+ transport = null,
1100
+ platform = null,
1101
+ background = {},
1102
+ onStateChanged = null
1103
+ } = {}) {
1104
+ const defaultPlatformClient = platform?.baseUrl ? createCallerPlatformClient(platform) : null;
1105
+ const defaultBackgroundPlatformClient = platform?.baseUrl && platform?.apiKey ? createCallerPlatformClient(platform) : null;
1106
+ const localFallbackClient = createLocalFallbackPlatformClient();
1107
+ const requestPlatformAuth = new Map();
1108
+
1109
+ function resolvePlatformConfig(req) {
1110
+ if (!platform?.baseUrl) {
1111
+ return null;
1112
+ }
1113
+
1114
+ const headerApiKey = req?.headers?.["x-platform-api-key"];
1115
+ if (typeof headerApiKey === "string" && headerApiKey.trim()) {
1116
+ return {
1117
+ baseUrl: platform.baseUrl,
1118
+ apiKey: headerApiKey.trim()
1119
+ };
1120
+ }
1121
+
1122
+ return platform;
1123
+ }
1124
+
1125
+ function resolvePlatformClient(req) {
1126
+ if (isLocalOnlyRegistrationMode(process.env.CALLER_REGISTRATION_MODE) && localFallbackClient) {
1127
+ return localFallbackClient;
1128
+ }
1129
+ const resolved = resolvePlatformConfig(req);
1130
+ if (!resolved?.baseUrl) {
1131
+ return localFallbackClient;
1132
+ }
1133
+ if (resolved === platform && defaultPlatformClient) {
1134
+ return defaultPlatformClient;
1135
+ }
1136
+ return createCallerPlatformClient(resolved);
1137
+ }
1138
+ const server = http.createServer(async (req, res) => {
1139
+ const method = req.method || "GET";
1140
+ const url = new URL(req.url || "/", "http://localhost");
1141
+ const pathname = url.pathname;
1142
+
1143
+ try {
1144
+ if (method === "OPTIONS") {
1145
+ res.writeHead(204, {
1146
+ "access-control-allow-origin": "*",
1147
+ "access-control-allow-methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
1148
+ "access-control-allow-headers": "Content-Type, Authorization, X-Platform-Api-Key"
1149
+ });
1150
+ res.end();
1151
+ return;
1152
+ }
1153
+
1154
+ if (method === "GET" && pathname === "/healthz") {
1155
+ sendJson(res, 200, { ok: true, service: serviceName });
1156
+ return;
1157
+ }
1158
+
1159
+ if (method === "GET" && pathname === "/readyz") {
1160
+ sendJson(res, 200, { ready: true, service: serviceName });
1161
+ return;
1162
+ }
1163
+
1164
+ if (method === "GET" && pathname === "/") {
1165
+ const platformClient = resolvePlatformClient(req);
1166
+ sendJson(res, 200, {
1167
+ service: serviceName,
1168
+ status: "running",
1169
+ config,
1170
+ platform: platformClient ? { configured: true, base_url: platformClient.config?.baseUrl || null } : { configured: false },
1171
+ local_defaults: {
1172
+ caller_contact_email: process.env.CALLER_CONTACT_EMAIL || null,
1173
+ platform_api_key_configured: Boolean(platform?.apiKey)
1174
+ }
1175
+ });
1176
+ return;
1177
+ }
1178
+
1179
+ if (method === "GET" && pathname === "/controller/hotlines") {
1180
+ const platformClient = resolvePlatformClient(req);
1181
+ if (!platformClient) {
1182
+ sendError(res, 409, "PLATFORM_NOT_CONFIGURED", "platform client is not configured");
1183
+ return;
1184
+ }
1185
+
1186
+ try {
1187
+ const catalog = await platformClient.listCatalogHotlines({
1188
+ status: url.searchParams.get("status") || undefined,
1189
+ availability_status: url.searchParams.get("availability_status") || undefined,
1190
+ task_type: url.searchParams.get("task_type") || undefined,
1191
+ capability: url.searchParams.get("capability") || undefined,
1192
+ tag: url.searchParams.get("tag") || undefined,
1193
+ responder_id: url.searchParams.get("responder_id") || undefined,
1194
+ hotline_id: url.searchParams.get("hotline_id") || undefined
1195
+ });
1196
+ sendJson(res, 200, catalog);
1197
+ } catch (error) {
1198
+ sendUpstreamError(res, error, "CALLER_PLATFORM_CATALOG_FAILED", "catalog query failed");
1199
+ }
1200
+ return;
1201
+ }
1202
+
1203
+ if (method === "POST" && pathname === "/controller/register") {
1204
+ const platformClient = resolvePlatformClient(req);
1205
+ if (!platformClient) {
1206
+ sendError(res, 409, "PLATFORM_NOT_CONFIGURED", "platform client is not configured");
1207
+ return;
1208
+ }
1209
+
1210
+ try {
1211
+ const body = await parseJsonBody(req);
1212
+ const registered = await platformClient.registerCaller(body);
1213
+ sendJson(res, 201, registered);
1214
+ } catch (error) {
1215
+ sendUpstreamError(res, error, "CALLER_PLATFORM_REGISTER_FAILED", "platform registration failed");
1216
+ }
1217
+ return;
1218
+ }
1219
+
1220
+ if (method === "POST" && pathname === "/controller/responder/register") {
1221
+ const platformClient = resolvePlatformClient(req);
1222
+ if (!platformClient) {
1223
+ sendError(res, 409, "PLATFORM_NOT_CONFIGURED", "platform client is not configured");
1224
+ return;
1225
+ }
1226
+
1227
+ try {
1228
+ const body = await parseJsonBody(req);
1229
+ const registered = await platformClient.registerResponder(body);
1230
+ sendJson(res, 201, registered);
1231
+ } catch (error) {
1232
+ sendUpstreamError(res, error, "CALLER_PLATFORM_RESPONDER_REGISTER_FAILED", "responder registration failed");
1233
+ }
1234
+ return;
1235
+ }
1236
+
1237
+ if (method === "POST" && pathname === "/controller/requests") {
1238
+ const body = await parseJsonBody(req);
1239
+ const record = createRequestRecord(config, body);
1240
+ state.requests.set(record.request_id, record);
1241
+ await persistCallerState(onStateChanged, state);
1242
+ sendJson(res, 201, record);
1243
+ return;
1244
+ }
1245
+
1246
+ if (method === "GET" && pathname === "/controller/requests") {
1247
+ const items = Array.from(state.requests.values());
1248
+ const platformClient = resolvePlatformClient(req);
1249
+ let mutated = false;
1250
+ for (const item of items) {
1251
+ const transition = await evaluateTimeoutsWithMetrics(item, config, platformClient);
1252
+ mutated ||= Boolean(transition);
1253
+ }
1254
+ if (mutated) {
1255
+ await persistCallerState(onStateChanged, state);
1256
+ }
1257
+ sendJson(res, 200, { items });
1258
+ return;
1259
+ }
1260
+
1261
+ const requestMatch = pathname.match(/^\/controller\/requests\/([^/]+)$/);
1262
+ if (method === "GET" && requestMatch) {
1263
+ const platformClient = resolvePlatformClient(req);
1264
+ const request = state.requests.get(requestMatch[1]);
1265
+ if (!request) {
1266
+ sendError(res, 404, "REQUEST_NOT_FOUND", "no request found with this id");
1267
+ return;
1268
+ }
1269
+ const transition = await evaluateTimeoutsWithMetrics(request, config, platformClient);
1270
+ if (transition) {
1271
+ await persistCallerState(onStateChanged, state);
1272
+ }
1273
+ sendJson(res, 200, request);
1274
+ return;
1275
+ }
1276
+
1277
+ const requestResultMatch = pathname.match(/^\/controller\/requests\/([^/]+)\/result$/);
1278
+ if (method === "GET" && requestResultMatch) {
1279
+ const request = state.requests.get(requestResultMatch[1]);
1280
+ if (!request) {
1281
+ sendError(res, 404, "REQUEST_NOT_FOUND", "no request found with this id");
1282
+ return;
1283
+ }
1284
+ if (!TERMINAL_STATUS_SET.has(request.status) || !request.result_package) {
1285
+ sendJson(res, 200, { available: false, status: request.status, result_package: null });
1286
+ return;
1287
+ }
1288
+ sendJson(res, 200, {
1289
+ available: true,
1290
+ status: request.status,
1291
+ result_package: request.result_package
1292
+ });
1293
+ return;
1294
+ }
1295
+
1296
+ const prepareMatch = pathname.match(/^\/controller\/requests\/([^/]+)\/prepare$/);
1297
+ if (method === "POST" && prepareMatch) {
1298
+ const platformClient = resolvePlatformClient(req);
1299
+ if (!platformClient) {
1300
+ sendError(res, 409, "PLATFORM_NOT_CONFIGURED", "platform client is not configured");
1301
+ return;
1302
+ }
1303
+
1304
+ const request = state.requests.get(prepareMatch[1]);
1305
+ if (!request) {
1306
+ sendError(res, 404, "REQUEST_NOT_FOUND", "no request found with this id");
1307
+ return;
1308
+ }
1309
+
1310
+ try {
1311
+ const body = await parseJsonBody(req);
1312
+ const platformConfig = resolvePlatformConfig(req);
1313
+ if (platformConfig?.apiKey) {
1314
+ requestPlatformAuth.set(request.request_id, platformConfig);
1315
+ }
1316
+ const prepared = await prepareCallerRequest(request, platformClient, body);
1317
+ await persistCallerState(onStateChanged, state);
1318
+ sendJson(res, 200, prepared);
1319
+ } catch (error) {
1320
+ if (error instanceof Error && error.message === "caller_prepare_requires_responder_and_hotline") {
1321
+ sendError(res, 400, "CONTRACT_INVALID_PREPARE_REQUEST", "responder_id and hotline_id are required");
1322
+ return;
1323
+ }
1324
+ if (error instanceof Error && error.message === "caller_signer_binding_mismatch") {
1325
+ sendError(res, 409, "SIGNER_BINDING_MISMATCH", "expected signer public key does not match catalog");
1326
+ return;
1327
+ }
1328
+ sendUpstreamError(res, error, "CALLER_PLATFORM_PREPARE_FAILED", "request preparation failed");
1329
+ }
1330
+ return;
1331
+ }
1332
+
1333
+ if (method === "POST" && pathname === "/controller/remote-requests") {
1334
+ const platformClient = resolvePlatformClient(req);
1335
+ if (!platformClient) {
1336
+ sendError(res, 409, "PLATFORM_NOT_CONFIGURED", "platform client is not configured");
1337
+ return;
1338
+ }
1339
+ if (!transport) {
1340
+ sendError(res, 409, "TRANSPORT_NOT_CONFIGURED", "transport adapter is not configured");
1341
+ return;
1342
+ }
1343
+
1344
+ try {
1345
+ const body = await parseJsonBody(req);
1346
+ const request = createRequestRecord(config, body);
1347
+ state.requests.set(request.request_id, request);
1348
+
1349
+ const platformConfig = resolvePlatformConfig(req);
1350
+ if (platformConfig?.apiKey) {
1351
+ requestPlatformAuth.set(request.request_id, platformConfig);
1352
+ }
1353
+
1354
+ const prepared = await prepareCallerRequest(request, platformClient, body);
1355
+ const contract = createTaskContractDraft(request, body);
1356
+ const envelope = buildDispatchEnvelope(request, {
1357
+ ...body,
1358
+ task_token: prepared.task_token
1359
+ });
1360
+
1361
+ await transport.send(envelope);
1362
+ if (!TERMINAL_STATUS_SET.has(request.status)) {
1363
+ setSentState(request);
1364
+ }
1365
+ await reportCallerMetric(platformClient, request, "caller.request.dispatched");
1366
+ await persistCallerState(onStateChanged, state);
1367
+
1368
+ sendJson(res, 201, {
1369
+ request_id: request.request_id,
1370
+ request,
1371
+ task_token: prepared.task_token,
1372
+ delivery_meta: prepared.delivery_meta,
1373
+ contract,
1374
+ envelope
1375
+ });
1376
+ } catch (error) {
1377
+ if (error instanceof Error && error.message === "caller_prepare_requires_responder_and_hotline") {
1378
+ sendError(res, 400, "CONTRACT_INVALID_REMOTE_REQUEST", "responder_id and hotline_id are required");
1379
+ return;
1380
+ }
1381
+ if (error instanceof Error && error.message === "caller_signer_binding_mismatch") {
1382
+ sendError(res, 409, "SIGNER_BINDING_MISMATCH", "expected signer public key does not match catalog");
1383
+ return;
1384
+ }
1385
+ sendUpstreamError(res, error, "CALLER_REMOTE_REQUEST_FAILED", "remote request dispatch failed");
1386
+ }
1387
+ return;
1388
+ }
1389
+
1390
+ const contractMatch = pathname.match(/^\/controller\/requests\/([^/]+)\/contract-draft$/);
1391
+ if (method === "POST" && contractMatch) {
1392
+ const request = state.requests.get(contractMatch[1]);
1393
+ if (!request) {
1394
+ sendError(res, 404, "REQUEST_NOT_FOUND", "no request found with this id");
1395
+ return;
1396
+ }
1397
+
1398
+ const body = await parseJsonBody(req);
1399
+ const contract = createTaskContractDraft(request, body);
1400
+ await persistCallerState(onStateChanged, state);
1401
+ sendJson(res, 200, { request_id: request.request_id, contract });
1402
+ return;
1403
+ }
1404
+
1405
+ const markSentMatch = pathname.match(/^\/controller\/requests\/([^/]+)\/mark-sent$/);
1406
+ if (method === "POST" && markSentMatch) {
1407
+ const request = state.requests.get(markSentMatch[1]);
1408
+ if (!request) {
1409
+ sendError(res, 404, "REQUEST_NOT_FOUND", "no request found with this id");
1410
+ return;
1411
+ }
1412
+
1413
+ if (!TERMINAL_STATUS_SET.has(request.status)) {
1414
+ setSentState(request);
1415
+ await persistCallerState(onStateChanged, state);
1416
+ }
1417
+
1418
+ sendJson(res, 200, request);
1419
+ return;
1420
+ }
1421
+
1422
+ const dispatchMatch = pathname.match(/^\/controller\/requests\/([^/]+)\/dispatch$/);
1423
+ if (method === "POST" && dispatchMatch) {
1424
+ const platformClient = resolvePlatformClient(req);
1425
+ const request = state.requests.get(dispatchMatch[1]);
1426
+ if (!request) {
1427
+ sendError(res, 404, "REQUEST_NOT_FOUND", "no request found with this id");
1428
+ return;
1429
+ }
1430
+
1431
+ if (!transport) {
1432
+ sendError(res, 409, "TRANSPORT_NOT_CONFIGURED", "transport adapter is not configured");
1433
+ return;
1434
+ }
1435
+
1436
+ const body = await parseJsonBody(req);
1437
+ const envelope = buildDispatchEnvelope(request, body);
1438
+
1439
+ await transport.send(envelope);
1440
+
1441
+ if (!TERMINAL_STATUS_SET.has(request.status)) {
1442
+ setSentState(request);
1443
+ }
1444
+ await reportCallerMetric(platformClient, request, "caller.request.dispatched");
1445
+ await persistCallerState(onStateChanged, state);
1446
+
1447
+ sendJson(res, 202, { accepted: true, envelope, request });
1448
+ return;
1449
+ }
1450
+
1451
+ const syncEventsMatch = pathname.match(/^\/controller\/requests\/([^/]+)\/sync-events$/);
1452
+ if (method === "POST" && syncEventsMatch) {
1453
+ const platformClient = resolvePlatformClient(req);
1454
+ if (!platformClient) {
1455
+ sendError(res, 409, "PLATFORM_NOT_CONFIGURED", "platform client is not configured");
1456
+ return;
1457
+ }
1458
+
1459
+ const request = state.requests.get(syncEventsMatch[1]);
1460
+ if (!request) {
1461
+ sendError(res, 404, "REQUEST_NOT_FOUND", "no request found with this id");
1462
+ return;
1463
+ }
1464
+
1465
+ try {
1466
+ const synced = await syncCallerRequestEvents(request, platformClient);
1467
+ if (synced.acked) {
1468
+ await reportCallerMetric(platformClient, request, "caller.request.acked");
1469
+ }
1470
+ await persistCallerState(onStateChanged, state);
1471
+ sendJson(res, 200, synced);
1472
+ } catch (error) {
1473
+ sendUpstreamError(res, error, "CALLER_PLATFORM_EVENTS_FAILED", "event sync failed");
1474
+ }
1475
+ return;
1476
+ }
1477
+
1478
+ const ackMatch = pathname.match(/^\/controller\/requests\/([^/]+)\/ack$/);
1479
+ if (method === "POST" && ackMatch) {
1480
+ const platformClient = resolvePlatformClient(req);
1481
+ const request = state.requests.get(ackMatch[1]);
1482
+ if (!request) {
1483
+ sendError(res, 404, "REQUEST_NOT_FOUND", "no request found with this id");
1484
+ return;
1485
+ }
1486
+
1487
+ if (!TERMINAL_STATUS_SET.has(request.status)) {
1488
+ request.status = "ACKED";
1489
+ request.acknowledged_at = nowIso();
1490
+ request.last_error_code = null;
1491
+ markUpdated(request, "ACKED");
1492
+ }
1493
+ await reportCallerMetric(platformClient, request, "caller.request.acked");
1494
+ await persistCallerState(onStateChanged, state);
1495
+
1496
+ sendJson(res, 200, request);
1497
+ return;
1498
+ }
1499
+
1500
+ if (method === "POST" && requestResultMatch) {
1501
+ const platformClient = resolvePlatformClient(req);
1502
+ const request = state.requests.get(requestResultMatch[1]);
1503
+ if (!request) {
1504
+ sendError(res, 404, "REQUEST_NOT_FOUND", "no request found with this id");
1505
+ return;
1506
+ }
1507
+
1508
+ if (TERMINAL_STATUS_SET.has(request.status)) {
1509
+ sendError(res, 409, "REQUEST_ALREADY_TERMINAL", "request has already reached a terminal state", { status: request.status });
1510
+ return;
1511
+ }
1512
+
1513
+ const body = await parseJsonBody(req);
1514
+ const transition = applyResultPackage(request, body);
1515
+ if (transition?.eventType) {
1516
+ await reportCallerMetric(platformClient, request, transition.eventType, { code: transition.code });
1517
+ }
1518
+ await persistCallerState(onStateChanged, state);
1519
+ sendJson(res, 200, request);
1520
+ return;
1521
+ }
1522
+
1523
+ if (method === "POST" && pathname === "/controller/inbox/pull") {
1524
+ if (!transport) {
1525
+ sendError(res, 409, "TRANSPORT_NOT_CONFIGURED", "transport adapter is not configured");
1526
+ return;
1527
+ }
1528
+
1529
+ const body = await parseJsonBody(req);
1530
+ const platformConfig = resolvePlatformConfig(req);
1531
+ const result = await pollCallerInbox(
1532
+ state,
1533
+ transport,
1534
+ () => (platformConfig?.apiKey ? createCallerPlatformClient(platformConfig) : null),
1535
+ onStateChanged,
1536
+ body.receiver || "caller-controller"
1537
+ );
1538
+ sendJson(res, 200, { accepted: result.accepted });
1539
+ return;
1540
+ }
1541
+
1542
+ const timeoutMatch = pathname.match(/^\/controller\/requests\/([^/]+)\/timeout-decision$/);
1543
+ if (method === "POST" && timeoutMatch) {
1544
+ const platformClient = resolvePlatformClient(req);
1545
+ const request = state.requests.get(timeoutMatch[1]);
1546
+ if (!request) {
1547
+ sendError(res, 404, "REQUEST_NOT_FOUND", "no request found with this id");
1548
+ return;
1549
+ }
1550
+
1551
+ const body = await parseJsonBody(req);
1552
+ const continueWait = body.continue_wait === true;
1553
+
1554
+ request.timeout_decision = continueWait ? "continue_wait" : "stop_wait";
1555
+ request.needs_timeout_confirmation = false;
1556
+ if (!continueWait && !TERMINAL_STATUS_SET.has(request.status)) {
1557
+ request.status = "TIMED_OUT";
1558
+ request.last_error_code = "EXEC_TIMEOUT_MANUAL_STOP";
1559
+ request.timed_out_at = nowIso();
1560
+ }
1561
+ markUpdated(request, continueWait ? "TIMEOUT_DECISION_CONTINUE" : "TIMEOUT_DECISION_STOP");
1562
+ if (!continueWait) {
1563
+ await reportCallerMetric(platformClient, request, "caller.request.timed_out", {
1564
+ code: request.last_error_code
1565
+ });
1566
+ }
1567
+ await persistCallerState(onStateChanged, state);
1568
+
1569
+ sendJson(res, 200, request);
1570
+ return;
1571
+ }
1572
+
1573
+ sendError(res, 404, "not_found", "no matching route", { path: pathname });
1574
+ } catch (error) {
1575
+ if (error.message === "invalid_json") {
1576
+ sendError(res, 400, "CONTRACT_INVALID_JSON", "request body is not valid JSON");
1577
+ return;
1578
+ }
1579
+
1580
+ sendError(res, 500, "CALLER_CONTROLLER_INTERNAL_ERROR", error instanceof Error ? error.message : "unknown_error", { retryable: true });
1581
+ }
1582
+ });
1583
+
1584
+ const backgroundEnabled = background.enabled === true;
1585
+ let stopBackground = () => {};
1586
+ if (backgroundEnabled) {
1587
+ stopBackground = startCallerBackgroundLoops({
1588
+ state,
1589
+ config,
1590
+ transport,
1591
+ receiver: background.receiver || "caller-controller",
1592
+ inboxPollIntervalMs: Number(background.inboxPollIntervalMs || 250),
1593
+ eventsSyncIntervalMs: Number(background.eventsSyncIntervalMs || 250),
1594
+ platformClientFactory: (request) => {
1595
+ const auth = requestPlatformAuth.get(request.request_id);
1596
+ if (auth?.baseUrl && auth?.apiKey) {
1597
+ return createCallerPlatformClient(auth);
1598
+ }
1599
+ if (defaultBackgroundPlatformClient) {
1600
+ return defaultBackgroundPlatformClient;
1601
+ }
1602
+ return null;
1603
+ },
1604
+ onStateChanged
1605
+ });
1606
+ server.on("close", () => {
1607
+ stopBackground();
1608
+ });
1609
+ }
1610
+
1611
+ return server;
1612
+ }