@lostgradient/weft 0.17.0 → 0.19.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 (172) hide show
  1. package/README.md +3 -1
  2. package/dist/cli/conformance.js +14 -18
  3. package/dist/cli/generated/operation-catalog.snapshot.json +331 -16
  4. package/dist/cli/generated/operation-client.generated.d.ts +39 -6
  5. package/dist/cli/generated/operation-client.generated.js +6 -0
  6. package/dist/core/context/child-workflow-pipe.d.ts +1 -1
  7. package/dist/core/context/child-workflow-pipe.js +1 -1
  8. package/dist/core/engine/construction.d.ts +2 -0
  9. package/dist/core/engine/construction.js +3 -0
  10. package/dist/core/engine/index.js +1 -0
  11. package/dist/core/engine/listing.d.ts +1 -0
  12. package/dist/core/engine/listing.js +6 -4
  13. package/dist/core/weft-error.d.ts +1 -1
  14. package/dist/core/weft-error.js +2 -1
  15. package/dist/core/worker-execution-dispatcher.d.ts +8 -0
  16. package/dist/core/worker-execution-dispatcher.js +4 -0
  17. package/dist/core/worker-execution-strategy-options.d.ts +16 -0
  18. package/dist/core/worker-execution-strategy.js +71 -89
  19. package/dist/core/worker-fault-handling.d.ts +45 -0
  20. package/dist/core/worker-fault-handling.js +88 -0
  21. package/dist/core/worker-protocol.d.ts +18 -1
  22. package/dist/core/worker-protocol.js +1 -1
  23. package/dist/core/worker-realm-readiness.d.ts +78 -0
  24. package/dist/core/worker-realm-readiness.js +127 -0
  25. package/dist/index.d.ts +3 -1
  26. package/dist/index.js +28 -1
  27. package/dist/json-schema.js +1 -1
  28. package/dist/runtime/portable.d.ts +14 -0
  29. package/dist/runtime/portable.js +10 -0
  30. package/dist/server/authorization-scope.d.ts +32 -2
  31. package/dist/server/index.d.ts +73 -8
  32. package/dist/server/index.js +11 -3
  33. package/dist/server/operations/get-principal.d.ts +75 -0
  34. package/dist/server/operations/get-principal.js +41 -0
  35. package/dist/server/operations/get-task-diagnostics.d.ts +43 -18
  36. package/dist/server/operations/get-task-diagnostics.js +87 -126
  37. package/dist/server/operations/get-worker-diagnostics.d.ts +177 -0
  38. package/dist/server/operations/get-worker-diagnostics.js +117 -0
  39. package/dist/server/operations/list-worker-registration-rejections.d.ts +79 -0
  40. package/dist/server/operations/list-worker-registration-rejections.js +60 -0
  41. package/dist/server/operations/list-workers.js +1 -3
  42. package/dist/server/operations/static-registrations.d.ts +28 -0
  43. package/dist/server/operations/static-registrations.js +248 -0
  44. package/dist/server/rest-bindings.d.ts +5 -0
  45. package/dist/server/rest-bindings.js +34 -313
  46. package/dist/server/runtime/authentication-bridge.js +6 -14
  47. package/dist/server/runtime/context.d.ts +23 -0
  48. package/dist/server/runtime/task-dispatch.d.ts +7 -1
  49. package/dist/server/runtime/task-dispatch.js +126 -55
  50. package/dist/server/runtime/task-ledger-completion.d.ts +68 -0
  51. package/dist/server/runtime/task-ledger-completion.js +72 -0
  52. package/dist/server/runtime/task-ledger-recovery.d.ts +55 -0
  53. package/dist/server/runtime/task-ledger-recovery.js +73 -0
  54. package/dist/server/runtime/task-ledger-runtime.d.ts +64 -0
  55. package/dist/server/runtime/task-ledger-runtime.js +34 -0
  56. package/dist/server/runtime/task-metrics.d.ts +6 -4
  57. package/dist/server/runtime/task-polling.d.ts +14 -12
  58. package/dist/server/runtime/task-polling.js +106 -82
  59. package/dist/server/runtime/task-reconciliation.d.ts +35 -8
  60. package/dist/server/runtime/task-reconciliation.js +75 -74
  61. package/dist/server/runtime/task-result-resolution.d.ts +2 -10
  62. package/dist/server/runtime/task-result-resolution.js +0 -67
  63. package/dist/server/runtime/task-result-view.d.ts +82 -0
  64. package/dist/server/runtime/task-result-view.js +48 -0
  65. package/dist/server/runtime/websocket-worker-messaging.d.ts +16 -0
  66. package/dist/server/runtime/websocket-worker-messaging.js +32 -0
  67. package/dist/server/runtime/websocket-worker-registration.d.ts +41 -0
  68. package/dist/server/runtime/websocket-worker-registration.js +134 -0
  69. package/dist/server/runtime/websocket-worker.d.ts +0 -1
  70. package/dist/server/runtime/websocket-worker.js +64 -116
  71. package/dist/server/serve-internals.d.ts +9 -6
  72. package/dist/server/serve-internals.js +14 -40
  73. package/dist/server/task-ledger-codec.d.ts +40 -0
  74. package/dist/server/task-ledger-codec.js +195 -0
  75. package/dist/server/task-ledger-keys.d.ts +13 -0
  76. package/dist/server/task-ledger-keys.js +5 -0
  77. package/dist/server/task-ledger-limits.d.ts +34 -0
  78. package/dist/server/task-ledger-limits.js +5 -0
  79. package/dist/server/task-ledger-transition-helpers.d.ts +38 -0
  80. package/dist/server/task-ledger-transition-helpers.js +42 -0
  81. package/dist/server/task-ledger-transitions-cancellation.d.ts +31 -0
  82. package/dist/server/task-ledger-transitions-cancellation.js +73 -0
  83. package/dist/server/task-ledger-transitions.d.ts +129 -0
  84. package/dist/server/task-ledger-transitions.js +162 -0
  85. package/dist/server/task-ledger-types.d.ts +220 -0
  86. package/dist/server/task-ledger-types.js +1 -0
  87. package/dist/server/task-ledger.d.ts +17 -0
  88. package/dist/server/task-ledger.js +24 -0
  89. package/dist/server/task-state.d.ts +25 -152
  90. package/dist/server/task-state.js +0 -188
  91. package/dist/server/worker-admission-policy.d.ts +70 -0
  92. package/dist/server/worker-admission-policy.js +0 -0
  93. package/dist/storage/compressed-storage.js +1 -1
  94. package/dist/storage/index.d.ts +0 -2
  95. package/dist/storage/interface.d.ts +0 -2
  96. package/dist/storage/interface.js +1 -1
  97. package/dist/storage/lmdb.js +1 -1
  98. package/dist/storage/memory.js +1 -1
  99. package/dist/storage/neon.js +1 -1
  100. package/dist/storage/postgres.js +1 -1
  101. package/dist/storage/resolve.js +1 -1
  102. package/dist/storage/scoped-storage.js +1 -1
  103. package/dist/storage/testing.js +1 -1
  104. package/dist/storage/turso.js +1 -1
  105. package/dist/storage/web-extension.js +2 -2
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/dist/web-extension.js +1 -1
  109. package/dist/worker/index.d.ts +2 -0
  110. package/dist/worker/index.js +30 -4
  111. package/dist/worker/manifest/canonical-json.d.ts +20 -0
  112. package/dist/worker/manifest/canonical-json.js +13 -0
  113. package/dist/worker/manifest/capabilities.d.ts +24 -0
  114. package/dist/worker/manifest/capabilities.js +74 -0
  115. package/dist/worker/manifest/content-digest.d.ts +20 -0
  116. package/dist/worker/manifest/content-digest.js +13 -0
  117. package/dist/worker/manifest/declared-shape-digest.d.ts +14 -0
  118. package/dist/worker/manifest/declared-shape-digest.js +4 -0
  119. package/dist/worker/manifest/digest.d.ts +82 -0
  120. package/dist/worker/manifest/digest.js +9 -0
  121. package/dist/worker/manifest/execution-identity.d.ts +92 -0
  122. package/dist/worker/manifest/execution-identity.js +36 -0
  123. package/dist/worker/manifest/failure.d.ts +59 -0
  124. package/dist/worker/manifest/failure.js +4 -0
  125. package/dist/worker/manifest/index.d.ts +22 -0
  126. package/dist/worker/manifest/index.js +23 -0
  127. package/dist/worker/manifest/internal-realm.d.ts +50 -0
  128. package/dist/worker/manifest/internal-realm.js +39 -0
  129. package/dist/worker/manifest/is-record.d.ts +12 -0
  130. package/dist/worker/manifest/is-record.js +6 -0
  131. package/dist/worker/manifest/json-scan.d.ts +26 -0
  132. package/dist/worker/manifest/json-scan.js +88 -0
  133. package/dist/worker/manifest/limits.d.ts +138 -0
  134. package/dist/worker/manifest/limits.js +1 -0
  135. package/dist/worker/manifest/normalize.d.ts +63 -0
  136. package/dist/worker/manifest/normalize.js +73 -0
  137. package/dist/worker/manifest/parse-json.d.ts +24 -0
  138. package/dist/worker/manifest/parse-json.js +15 -0
  139. package/dist/worker/manifest/parse.d.ts +86 -0
  140. package/dist/worker/manifest/parse.js +188 -0
  141. package/dist/worker/manifest/registry-contract-builder.d.ts +120 -0
  142. package/dist/worker/manifest/registry-contract-builder.js +65 -0
  143. package/dist/worker/manifest/types.d.ts +263 -0
  144. package/dist/worker/manifest/types.js +1 -0
  145. package/dist/worker/manifest/utf8.d.ts +13 -0
  146. package/dist/worker/manifest/utf8.js +7 -0
  147. package/dist/worker/options.d.ts +55 -8
  148. package/dist/worker/options.js +65 -9
  149. package/dist/worker/protocol-internals.d.ts +1 -2
  150. package/dist/worker/protocol-internals.js +0 -5
  151. package/dist/worker/protocol-messages.d.ts +33 -17
  152. package/dist/worker/protocol-schemas.d.ts +36 -74
  153. package/dist/worker/protocol-schemas.js +25 -18
  154. package/dist/worker/protocol-version.d.ts +18 -14
  155. package/dist/worker/protocol-version.js +1 -1
  156. package/dist/worker/protocol.js +1 -1
  157. package/dist/worker/registry/deployment-consistency.d.ts +78 -0
  158. package/dist/worker/registry/deployment-consistency.js +27 -0
  159. package/dist/worker/registry/drain.d.ts +14 -0
  160. package/dist/worker/registry/drain.js +24 -0
  161. package/dist/worker/registry/rejections.d.ts +31 -0
  162. package/dist/worker/registry/rejections.js +10 -0
  163. package/dist/worker/registry/summary.d.ts +0 -3
  164. package/dist/worker/registry/summary.js +2 -5
  165. package/dist/worker/registry/types.d.ts +10 -3
  166. package/dist/worker/registry.d.ts +47 -2
  167. package/dist/worker/registry.js +45 -32
  168. package/dist/workers/workflow-worker-entry.d.ts +12 -7
  169. package/dist/workers/workflow-worker-entry.js +9 -2
  170. package/package.json +3 -3
  171. package/dist/server/task-resolved-record.d.ts +0 -12
  172. package/dist/server/task-resolved-record.js +0 -35
@@ -0,0 +1,134 @@
1
+ import { WorkerConnectedEvent } from "../../core/events.js";
2
+ import { digestCanonicalWorkerManifest, parseWorkerManifest } from "../../worker/manifest/index.js";
3
+ import {
4
+ REMOTE_WORKER_PROTOCOL_VERSION
5
+ } from "../../worker/protocol.js";
6
+ import { isAuthenticated } from "../principal.js";
7
+ import { rejectRegistration, sendWorkerProtocolMessage } from "./websocket-worker-messaging.js";
8
+ const MAX_WORKER_CONCURRENCY = 1000, DEFAULT_WORKER_CONCURRENCY = 10;
9
+ function rejectAndRecordRegistration(context, ws, code, message, workerId, requestedProtocolVersion, identity) {
10
+ context.registry.recordRejection({
11
+ code,
12
+ ...workerId !== void 0 ? { workerId } : {},
13
+ rejectedAt: Date.now(),
14
+ queue: ws.data.queue ?? "default",
15
+ ...identity?.deploymentName !== void 0 ? { deploymentName: identity.deploymentName } : {},
16
+ ...identity?.buildId !== void 0 ? { buildId: identity.buildId } : {}
17
+ });
18
+ rejectRegistration(ws, code, message, requestedProtocolVersion);
19
+ }
20
+ function principalMayRegisterWorker(principal) {
21
+ if (principal === void 0)
22
+ return !0;
23
+ return isAuthenticated(principal) && principal.hasScope("workers:write");
24
+ }
25
+ function deriveActivitiesFromManifest(manifest) {
26
+ const activities = [];
27
+ for (const [workflowType, workflow] of Object.entries(manifest.workflows))
28
+ for (const activityName of Object.keys(workflow.activities))
29
+ activities.push(`${workflowType}.${activityName}`);
30
+ return activities;
31
+ }
32
+ function buildWorkerRegistrationInfo(message, manifest, acceptedManifestDigest, queue, concurrency) {
33
+ const runtimeVersion = manifest.runtime.version === "" ? void 0 : manifest.runtime.version;
34
+ return {
35
+ id: message.workerId,
36
+ queue,
37
+ activities: deriveActivitiesFromManifest(manifest),
38
+ concurrency,
39
+ deploymentName: manifest.deployment.name,
40
+ buildId: manifest.deployment.buildId,
41
+ ...runtimeVersion !== void 0 ? { runtimeVersion } : {},
42
+ manifest,
43
+ acceptedManifestDigest,
44
+ ...message.startedAt !== void 0 ? { startedAt: message.startedAt } : {},
45
+ capabilities: manifest.capabilities
46
+ };
47
+ }
48
+ function admitReconnectingSocket(context, ws, message, manifest) {
49
+ const pendingRequeue = context.pendingWorkerRequeues.get(message.workerId), isGracePeriodReconnect = pendingRequeue !== void 0;
50
+ if (isGracePeriodReconnect) {
51
+ clearTimeout(pendingRequeue);
52
+ context.pendingWorkerRequeues.delete(message.workerId);
53
+ }
54
+ const existingSocket = context.workerSockets.get(message.workerId);
55
+ if (!isGracePeriodReconnect && existingSocket !== void 0 && existingSocket !== ws) {
56
+ rejectAndRecordRegistration(context, ws, "invalid_registration", "workerId is already registered to an active connection", message.workerId, message.protocolVersion, { deploymentName: manifest.deployment.name, buildId: manifest.deployment.buildId });
57
+ return !1;
58
+ }
59
+ return !0;
60
+ }
61
+ function evaluateWorkerAdmissionPolicy(policy, request) {
62
+ try {
63
+ return policy(request);
64
+ } catch (error) {
65
+ return {
66
+ status: "rejected",
67
+ reason: `workerAdmissionPolicy threw: ${error instanceof Error ? error.message : String(error)}`
68
+ };
69
+ }
70
+ }
71
+ function commitWorkerRegistration(context, options, ws, message, manifest, queue, concurrency, acceptedManifestDigest) {
72
+ if (ws.readyState !== WebSocket.OPEN)
73
+ return;
74
+ if (!context.registry.checkDeploymentConsistency(manifest.deployment.name, manifest.deployment.buildId, manifest.deployment.artifactDigest).ok) {
75
+ rejectAndRecordRegistration(context, ws, "deployment_conflict", `Deployment "${manifest.deployment.name}" build "${manifest.deployment.buildId}" is already registered with a different artifact digest`, message.workerId, message.protocolVersion, { deploymentName: manifest.deployment.name, buildId: manifest.deployment.buildId });
76
+ return;
77
+ }
78
+ if (!admitReconnectingSocket(context, ws, message, manifest))
79
+ return;
80
+ context.registry.recordDeploymentConsistency(manifest.deployment.name, manifest.deployment.buildId, manifest.deployment.artifactDigest);
81
+ const registrationInfo = buildWorkerRegistrationInfo(message, manifest, acceptedManifestDigest, queue, concurrency);
82
+ ws.data.workerId = message.workerId;
83
+ ws.data.workerRegistered = !0;
84
+ ws.data.workerProtocolVersion = message.protocolVersion;
85
+ context.registry.register(registrationInfo);
86
+ context.workerSockets.set(message.workerId, ws);
87
+ sendWorkerProtocolMessage(ws, {
88
+ type: "registerAck",
89
+ protocolVersion: REMOTE_WORKER_PROTOCOL_VERSION,
90
+ workerId: message.workerId,
91
+ queue,
92
+ concurrency,
93
+ acceptedManifestDigest,
94
+ serverCapabilities: []
95
+ });
96
+ options.engine.dispatchEvent(new WorkerConnectedEvent(message.workerId, queue, registrationInfo.activities, concurrency));
97
+ }
98
+ export async function registerWorker(context, options, ws, message) {
99
+ try {
100
+ await context.taskLedgerRecovery.ready;
101
+ } catch (error) {
102
+ rejectAndRecordRegistration(context, ws, "registration_rejected", `Server task-ledger recovery failed \u2014 cannot admit new workers: ${error instanceof Error ? error.message : String(error)}`, message.workerId, message.protocolVersion);
103
+ return;
104
+ }
105
+ if (!principalMayRegisterWorker(ws.data.principal)) {
106
+ rejectAndRecordRegistration(context, ws, "invalid_registration", "Worker registration requires the workers:write scope", message.workerId, message.protocolVersion);
107
+ return;
108
+ }
109
+ const parsedManifest = parseWorkerManifest(message.manifest);
110
+ if (!parsedManifest.ok) {
111
+ rejectAndRecordRegistration(context, ws, "invalid_registration", parsedManifest.message, message.workerId, message.protocolVersion);
112
+ return;
113
+ }
114
+ const { manifest, canonicalJson } = parsedManifest;
115
+ if (manifest.protocolVersion !== message.protocolVersion) {
116
+ rejectAndRecordRegistration(context, ws, "invalid_registration", `Manifest declares protocolVersion ${String(manifest.protocolVersion)}, which does not match the register message's protocolVersion ${String(message.protocolVersion)}`, message.workerId, message.protocolVersion, { deploymentName: manifest.deployment.name, buildId: manifest.deployment.buildId });
117
+ return;
118
+ }
119
+ const queue = ws.data.queue ?? "default";
120
+ if (options.workerAdmissionPolicy !== void 0) {
121
+ const decision = evaluateWorkerAdmissionPolicy(options.workerAdmissionPolicy, {
122
+ principal: ws.data.principal,
123
+ workerId: message.workerId,
124
+ queue,
125
+ manifest
126
+ });
127
+ if (decision.status === "rejected") {
128
+ rejectAndRecordRegistration(context, ws, "registration_rejected", decision.reason, message.workerId, message.protocolVersion, { deploymentName: manifest.deployment.name, buildId: manifest.deployment.buildId });
129
+ return;
130
+ }
131
+ }
132
+ const rawConcurrency = message.concurrency ?? DEFAULT_WORKER_CONCURRENCY, clampedConcurrency = Math.min(Math.max(1, Math.floor(rawConcurrency)), MAX_WORKER_CONCURRENCY), acceptedManifestDigest = await digestCanonicalWorkerManifest(canonicalJson);
133
+ commitWorkerRegistration(context, options, ws, message, manifest, queue, clampedConcurrency, acceptedManifestDigest);
134
+ }
@@ -2,6 +2,5 @@ import type { ServerWebSocket } from 'bun';
2
2
  import type { ServeOptions } from '../index.ts';
3
3
  import type { WebSocketData } from '../json-rpc-websocket-runtime.ts';
4
4
  import type { ServerContext } from './context.ts';
5
- export { isInflightRecord } from '../task-state.ts';
6
5
  export { withRetry } from './retry.ts';
7
6
  export declare function handleWorkerWebSocketMessage(context: ServerContext, options: ServeOptions, ws: ServerWebSocket<WebSocketData>, rawMessage: string | Buffer, cleanupWorkflowIndex: (operationId: string) => void): void;
@@ -1,104 +1,31 @@
1
- import { decode, encode } from "../../core/codec.js";
2
- import { WorkerConnectedEvent } from "../../core/events.js";
3
- import { KEYS } from "../../storage/interface.js";
4
1
  import {
5
2
  REMOTE_WORKER_PROTOCOL_VERSION,
6
- REMOTE_WORKER_SUPPORTED_PROTOCOL_VERSIONS,
7
3
  parseWorkerToServerMessage
8
4
  } from "../../worker/protocol.js";
9
5
  import { workerProtocolIncompatibleMessage } from "../../worker/worker-protocol-incompatible-error.js";
10
- import { isAuthenticated } from "../principal.js";
11
- import { isInflightRecord, readInflightRecord } from "../task-state.js";
6
+ import { renewAttemptLease } from "../task-ledger-transitions.js";
12
7
  import { withRetry } from "./retry.js";
13
- import { recordWorkerCapacitySaturationMetric } from "./task-metrics.js";
14
8
  import {
15
- taskResultPayloadSizeError,
16
- transitionTaskResultToResolvedWithRetry
17
- } from "./task-result-resolution.js";
9
+ commitTaskLedgerCompletion,
10
+ dispatchTaskDeadLetteredEvent
11
+ } from "./task-ledger-completion.js";
12
+ import { commitTaskLedgerTransition } from "./task-ledger-runtime.js";
13
+ import {
14
+ recordTaskExecutionLatencyMetric,
15
+ recordWorkerCapacitySaturationMetric
16
+ } from "./task-metrics.js";
17
+ import { taskResultPayloadSizeError } from "./task-result-resolution.js";
18
18
  import { WORKER_STREAM_RE } from "./websocket-upgrade.js";
19
- const MAX_WORKER_CONCURRENCY = 1000, DEFAULT_WORKER_CONCURRENCY = 10, WORKER_PROTOCOL_CLOSE_CODE = 1002, WORKER_REGISTRATION_CLOSE_CODE = 1008;
19
+ import {
20
+ rejectProtocolMessage,
21
+ rejectRegistration,
22
+ sendWorkerProtocolMessage
23
+ } from "./websocket-worker-messaging.js";
24
+ import { registerWorker } from "./websocket-worker-registration.js";
20
25
  function isWorkerConnection(pathname) {
21
26
  return WORKER_STREAM_RE.test(pathname);
22
27
  }
23
- export { isInflightRecord } from "../task-state.js";
24
28
  export { withRetry } from "./retry.js";
25
- function sendWorkerProtocolMessage(ws, message) {
26
- ws.send(JSON.stringify(message));
27
- }
28
- function closeWorkerSocket(ws, code, reason) {
29
- try {
30
- ws.unsubscribe(ws.data.pathname);
31
- } catch {}
32
- ws.close(code, reason);
33
- setTimeout(() => {
34
- try {
35
- ws.terminate();
36
- } catch {}
37
- }, 10);
38
- }
39
- function rejectRegistration(ws, code, message, requestedProtocolVersion) {
40
- sendWorkerProtocolMessage(ws, {
41
- type: "registerError",
42
- code,
43
- message,
44
- supportedProtocolVersions: REMOTE_WORKER_SUPPORTED_PROTOCOL_VERSIONS,
45
- ...requestedProtocolVersion !== void 0 ? { requestedProtocolVersion } : {}
46
- });
47
- closeWorkerSocket(ws, WORKER_REGISTRATION_CLOSE_CODE, code);
48
- }
49
- function rejectProtocolMessage(ws, code, message) {
50
- sendWorkerProtocolMessage(ws, { type: "protocolError", code, message });
51
- closeWorkerSocket(ws, WORKER_PROTOCOL_CLOSE_CODE, code);
52
- }
53
- function principalMayRegisterWorker(principal) {
54
- if (principal === void 0)
55
- return !0;
56
- return isAuthenticated(principal) && principal.hasScope("workers:write");
57
- }
58
- function buildWorkerRegistrationInfo(message, queue, concurrency) {
59
- return {
60
- id: message.workerId,
61
- queue,
62
- activities: [...message.activities],
63
- concurrency,
64
- ...message.deploymentName !== void 0 ? { deploymentName: message.deploymentName } : {},
65
- ...message.buildId !== void 0 ? { buildId: message.buildId } : {},
66
- ...message.runtimeVersion !== void 0 ? { runtimeVersion: message.runtimeVersion } : {},
67
- ...message.gitSha !== void 0 ? { gitSha: message.gitSha } : {},
68
- ...message.startedAt !== void 0 ? { startedAt: message.startedAt } : {},
69
- ...message.capabilities !== void 0 ? { capabilities: message.capabilities } : {}
70
- };
71
- }
72
- function registerWorker(context, options, ws, message) {
73
- if (!principalMayRegisterWorker(ws.data.principal)) {
74
- rejectRegistration(ws, "invalid_registration", "Worker registration requires the workers:write scope", message.protocolVersion);
75
- return;
76
- }
77
- const rawConcurrency = message.concurrency ?? DEFAULT_WORKER_CONCURRENCY, clampedConcurrency = Math.min(Math.max(1, Math.floor(rawConcurrency)), MAX_WORKER_CONCURRENCY), queue = ws.data.queue ?? "default", pendingRequeue = context.pendingWorkerRequeues.get(message.workerId), isGracePeriodReconnect = pendingRequeue !== void 0;
78
- if (isGracePeriodReconnect) {
79
- clearTimeout(pendingRequeue);
80
- context.pendingWorkerRequeues.delete(message.workerId);
81
- }
82
- const existingSocket = context.workerSockets.get(message.workerId);
83
- if (!isGracePeriodReconnect && existingSocket !== void 0 && existingSocket !== ws) {
84
- rejectRegistration(ws, "invalid_registration", "workerId is already registered to an active connection", message.protocolVersion);
85
- return;
86
- }
87
- ws.data.workerId = message.workerId;
88
- ws.data.workerRegistered = !0;
89
- ws.data.workerProtocolVersion = message.protocolVersion;
90
- context.registry.register(buildWorkerRegistrationInfo(message, queue, clampedConcurrency));
91
- context.workerSockets.set(message.workerId, ws);
92
- sendWorkerProtocolMessage(ws, {
93
- type: "registerAck",
94
- protocolVersion: REMOTE_WORKER_PROTOCOL_VERSION,
95
- workerId: message.workerId,
96
- queue,
97
- activities: [...message.activities],
98
- concurrency: clampedConcurrency
99
- });
100
- options.engine.dispatchEvent(new WorkerConnectedEvent(message.workerId, queue, [...message.activities], clampedConcurrency));
101
- }
102
29
  function resolveTaskResultStatus(message) {
103
30
  return message.status === "completed" ? "completed" : "failed";
104
31
  }
@@ -121,9 +48,7 @@ function onTaskResultMessage(context, options, ws, message, cleanupWorkflowIndex
121
48
  return;
122
49
  }
123
50
  const resolvedStatus = resolveTaskResultStatus(message), payloadError = taskResultPayloadSizeError({
124
- operationId,
125
51
  status: resolvedStatus,
126
- resolutionReason: resolvedStatus,
127
52
  ...message.status === "completed" ? { value: message.value } : { error: message.error }
128
53
  }, context.payloadSizeMaxBytes);
129
54
  context.registry.completeTask(operationId);
@@ -131,30 +56,40 @@ function onTaskResultMessage(context, options, ws, message, cleanupWorkflowIndex
131
56
  cleanupWorkflowIndex(operationId);
132
57
  recordWorkerCapacitySaturationMetric(context.metricsCollector, context.registry);
133
58
  (async () => {
134
- const inflightRecord = await readInflightRecord(options.engine.storage, operationId);
135
59
  if (payloadError !== null) {
136
60
  sendWorkerProtocolMessage(ws, {
137
61
  type: "protocolError",
138
62
  code: "invalid_message",
139
63
  message: payloadError.message
140
64
  });
141
- await transitionTaskResultToResolvedWithRetry(context, options, {
65
+ const rejected = await commitTaskLedgerCompletion(options.engine.storage, {
142
66
  operationId,
67
+ attemptToken: message.attemptToken,
143
68
  status: "failed",
144
- resolutionReason: "failed",
145
- inflightRecord,
146
- error: payloadError.message,
147
- skipPayloadSizeCheck: !0
69
+ error: payloadError.message
148
70
  });
71
+ if (rejected.ok)
72
+ recordTaskExecutionLatencyMetric(context.metricsCollector, { startedAt: rejected.completing.startedAt }, Date.now());
73
+ else {
74
+ console.error(`[weft] Failed to persist oversized task result rejection for task "${operationId}":`, rejected.reason);
75
+ if (rejected.deadLettered !== void 0)
76
+ dispatchTaskDeadLetteredEvent(options, operationId, rejected.deadLettered, workerId);
77
+ }
149
78
  return;
150
79
  }
151
- await transitionTaskResultToResolvedWithRetry(context, options, {
80
+ const committed = await commitTaskLedgerCompletion(options.engine.storage, {
152
81
  operationId,
82
+ attemptToken: message.attemptToken,
153
83
  status: resolvedStatus,
154
- resolutionReason: resolvedStatus,
155
- inflightRecord,
156
84
  ...message.status === "completed" ? { value: message.value } : { error: message.error }
157
85
  });
86
+ if (committed.ok)
87
+ recordTaskExecutionLatencyMetric(context.metricsCollector, { startedAt: committed.completing.startedAt }, Date.now());
88
+ else {
89
+ console.error(`[weft] Failed to commit task result for "${operationId}" through the durable ledger:`, committed.reason);
90
+ if (committed.deadLettered !== void 0)
91
+ dispatchTaskDeadLetteredEvent(options, operationId, committed.deadLettered, workerId);
92
+ }
158
93
  })().catch((error) => {
159
94
  console.error(`[weft] Failed to transition task "${operationId}" to resolved \u2014 inflight record may leak:`, error);
160
95
  });
@@ -169,29 +104,40 @@ function onHeartbeatMessage(context, options, ws, _message) {
169
104
  if (newDeadline !== void 0) {
170
105
  context.deadlineTracker.remove(task.operationId);
171
106
  context.deadlineTracker.add({ operationId: task.operationId, deadline: newDeadline });
172
- const opId = task.operationId, heartbeatWorkerId = ws.data.workerId;
107
+ const opId = task.operationId, heartbeatWorkerId = ws.data.workerId, attemptToken = task.attemptToken;
173
108
  withRetry(async () => {
174
109
  if (!context.registry.isAssigned(opId))
175
110
  return;
176
111
  if (!context.registry.getWorkerTasks(heartbeatWorkerId ?? "").find((trackedTask) => trackedTask.operationId === opId))
177
112
  return;
178
- const inflightKey = KEYS.operationInflight(opId), existing = await options.engine.storage.get(inflightKey);
179
- if (existing) {
180
- const decoded = decode(existing);
181
- if (!isInflightRecord(decoded)) {
182
- console.error(`[weft] Corrupt inflight record for task "${opId}" during heartbeat \u2014 skipping visibility extension`);
183
- return;
184
- }
185
- const updated = { ...decoded, deadline: newDeadline, lastHeartbeatAt: Date.now() };
186
- await options.engine.storage.put(inflightKey, encode(updated));
187
- }
113
+ await commitTaskLedgerTransition(options.engine.storage, opId, (current, now) => renewAttemptLease(current, {
114
+ attemptToken,
115
+ workerSessionId: heartbeatWorkerId ?? "",
116
+ leaseDurationMilliseconds: task.visibilityTimeout
117
+ }, now), 1);
188
118
  }, `extend visibility for task "${opId}"`).catch((error) => {
189
119
  console.error(`[weft] Failed to extend visibility for task "${opId}":`, error);
190
120
  });
191
121
  }
192
122
  }
193
123
  }
194
- function parseAndValidateWorkerFrame(ws, rawMessage) {
124
+ function extractOptionalWorkerId(parsed) {
125
+ if (typeof parsed !== "object" || parsed === null)
126
+ return;
127
+ const workerId = parsed.workerId;
128
+ return typeof workerId === "string" && workerId.length > 0 ? workerId : void 0;
129
+ }
130
+ function recordAndRejectRegistrationFrame(context, ws, parsed, code, message, requestedProtocolVersion) {
131
+ const rejectedWorkerId = extractOptionalWorkerId(parsed);
132
+ context.registry.recordRejection({
133
+ code,
134
+ ...rejectedWorkerId !== void 0 ? { workerId: rejectedWorkerId } : {},
135
+ rejectedAt: Date.now(),
136
+ queue: ws.data.queue ?? "default"
137
+ });
138
+ rejectRegistration(ws, code, message, requestedProtocolVersion);
139
+ }
140
+ function parseAndValidateWorkerFrame(context, ws, rawMessage) {
195
141
  const text = typeof rawMessage === "string" ? rawMessage : new TextDecoder().decode(rawMessage);
196
142
  let parsed;
197
143
  try {
@@ -202,12 +148,12 @@ function parseAndValidateWorkerFrame(ws, rawMessage) {
202
148
  }
203
149
  const result = parseWorkerToServerMessage(parsed);
204
150
  if (!result.ok) {
205
- if (result.error.code === "invalid_registration" || result.error.code === "unsupported_protocol_version") {
151
+ if (result.error.code === "invalid_registration" || result.error.code === "unsupported_protocol_version" || result.error.code === "deployment_conflict" || result.error.code === "registration_rejected") {
206
152
  const message = result.error.code === "unsupported_protocol_version" ? workerProtocolIncompatibleMessage({
207
153
  expected: REMOTE_WORKER_PROTOCOL_VERSION,
208
154
  received: result.error.requestedProtocolVersion
209
155
  }) : result.error.message;
210
- rejectRegistration(ws, result.error.code, message, result.error.requestedProtocolVersion);
156
+ recordAndRejectRegistrationFrame(context, ws, parsed, result.error.code, message, result.error.requestedProtocolVersion);
211
157
  return { ok: !1 };
212
158
  }
213
159
  rejectProtocolMessage(ws, result.error.code, result.error.message);
@@ -218,7 +164,7 @@ function parseAndValidateWorkerFrame(ws, rawMessage) {
218
164
  export function handleWorkerWebSocketMessage(context, options, ws, rawMessage, cleanupWorkflowIndex) {
219
165
  if (!isWorkerConnection(ws.data.pathname))
220
166
  return;
221
- const parsed = parseAndValidateWorkerFrame(ws, rawMessage);
167
+ const parsed = parseAndValidateWorkerFrame(context, ws, rawMessage);
222
168
  if (!parsed.ok)
223
169
  return;
224
170
  const { message } = parsed;
@@ -228,7 +174,9 @@ export function handleWorkerWebSocketMessage(context, options, ws, rawMessage, c
228
174
  }
229
175
  switch (message.type) {
230
176
  case "register": {
231
- registerWorker(context, options, ws, message);
177
+ registerWorker(context, options, ws, message).catch((error) => {
178
+ console.error(`[weft] Failed to register worker "${message.workerId}":`, error);
179
+ });
232
180
  break;
233
181
  }
234
182
  case "taskResult": {
@@ -81,6 +81,15 @@ export declare function resolveNetworkConfig(options: ServeOptions): ResolvedNet
81
81
  /**
82
82
  * Builds the initial `ServerContext` from resolved options. All mutable state
83
83
  * maps and the metrics collector are allocated here so `serve()` stays linear.
84
+ *
85
+ * Also starts startup task-ledger recovery (WFT-23) against the fully
86
+ * constructed context before returning it. Recovery runs asynchronously —
87
+ * this function itself stays synchronous, same as `serve()` — but its
88
+ * outcome is captured into `context.taskLedgerRecovery.ready` immediately,
89
+ * before any request can reach a gated entry point, rather than in a
90
+ * separate call `serve()` makes later (as the retired `restoreInflightTasks`
91
+ * required): starting it here minimizes the window between context
92
+ * construction and recovery beginning.
84
93
  */
85
94
  export declare function buildServerContext(options: ResolvedServeOptions, serverMetricsCollector: MetricsCollector): ServerContext;
86
95
  /**
@@ -113,9 +122,3 @@ export declare function buildBunServeConfig(port: number, hostname: string, deve
113
122
  * the most-recently-registered item is torn down first.
114
123
  */
115
124
  export declare function registerStackDisposers(stack: AsyncDisposableStack, context: ServerContext, options: ServeOptions, broadcastingHandle: EventBroadcastingHandle, onOperationCleanup: (operationId: string) => void): void;
116
- /**
117
- * Restores persisted in-flight task records from storage into the in-memory
118
- * context. Records whose deadline has already elapsed are removed from storage;
119
- * the engine will retry them on the next dispatch cycle.
120
- */
121
- export declare function restoreInflightTasks(context: ServerContext, options: ServeOptions): void;
@@ -1,4 +1,3 @@
1
- import { decode } from "../core/codec.js";
2
1
  import { getEnginePayloadSizeMaxBytes } from "../core/engine/payload-size-policy.js";
3
2
  import { createMcpSessionManager } from "../mcp/session.js";
4
3
  import { createMetricsCollectorExporter, MetricsCollector } from "../observability/metrics.js";
@@ -25,13 +24,13 @@ import {
25
24
  registerWorkflowEventLifecycle
26
25
  } from "./runtime/event-broadcasting.js";
27
26
  import { shutdownAllWorkers } from "./runtime/shutdown.js";
27
+ import { runTaskLedgerRecovery } from "./runtime/task-ledger-recovery.js";
28
28
  import {
29
29
  consumeManualTaskReconciliationForTesting as consumeManual,
30
30
  reconcileOrphanedRecords,
31
31
  scanExpiredTasks
32
32
  } from "./runtime/task-reconciliation.js";
33
33
  import { DEFAULT_MAX_STREAM_CONNECTIONS_PER_WORKFLOW } from "./runtime/websocket-stream.js";
34
- import { isInflightRecord, withRetry } from "./runtime/websocket-worker.js";
35
34
  import { TaskQueue } from "./task-queue.js";
36
35
  import { createWorkflowEventFeed } from "./workflow-event-feed.js";
37
36
  const RECONCILIATION_MULTIPLIER = 12, DEFAULT_WORKER_RECONNECT_GRACE_PERIOD_MS = 2000, MAX_WORKER_RECONNECT_GRACE_PERIOD_MS = 5000;
@@ -90,7 +89,13 @@ export function resolveNetworkConfig(options) {
90
89
  }
91
90
  export function buildServerContext(options, serverMetricsCollector) {
92
91
  const workerRegistry = new WorkerRegistry(options.routingPolicy !== void 0 ? { policy: options.routingPolicy } : void 0), taskQueue = new TaskQueue(options.schedulingPolicy !== void 0 ? { schedulingPolicy: options.schedulingPolicy } : void 0), engine = options.engine, eventFeedBackend = createEngineEventFeedBackend(engine);
93
- return {
92
+ let resolveTaskLedgerRecovery, rejectTaskLedgerRecovery;
93
+ const taskLedgerRecoveryReady = new Promise((resolve, reject) => {
94
+ resolveTaskLedgerRecovery = resolve;
95
+ rejectTaskLedgerRecovery = reject;
96
+ });
97
+ taskLedgerRecoveryReady.catch(() => {});
98
+ const context = {
94
99
  registry: workerRegistry,
95
100
  taskQueue,
96
101
  workerSockets: new Map,
@@ -125,8 +130,12 @@ export function buildServerContext(options, serverMetricsCollector) {
125
130
  pendingWorkerRequeues: new Map,
126
131
  scanRunning: !1,
127
132
  processingOperations: new Set,
128
- reconciliationRunning: !1
133
+ reconciliationRunning: !1,
134
+ taskLedgerRecovery: { ready: taskLedgerRecoveryReady },
135
+ stopping: !1
129
136
  };
137
+ runTaskLedgerRecovery(context, options).then(resolveTaskLedgerRecovery, rejectTaskLedgerRecovery);
138
+ return context;
130
139
  }
131
140
  export function buildFetchHandler(serverHolder, context, options) {
132
141
  return (request) => {
@@ -176,6 +185,7 @@ export function registerStackDisposers(stack, context, options, broadcastingHand
176
185
  reconcileOrphanedRecords(context, options, onOperationCleanup);
177
186
  }, reconciliationIntervalMs) : void 0;
178
187
  stack.defer(() => {
188
+ context.stopping = !0;
179
189
  if (visibilityPollHandle !== void 0)
180
190
  clearInterval(visibilityPollHandle);
181
191
  if (reconciliationHandle !== void 0)
@@ -191,39 +201,3 @@ export function registerStackDisposers(stack, context, options, broadcastingHand
191
201
  context.rateLimiter?.dispose();
192
202
  });
193
203
  }
194
- export function restoreInflightTasks(context, options) {
195
- withRetry(async () => {
196
- for await (const [key, value] of options.engine.storage.scan("op:inflight:")) {
197
- const decoded = decode(value);
198
- if (!isInflightRecord(decoded)) {
199
- console.error(`[weft] Corrupt inflight record at "${key}" during restore \u2014 skipping`);
200
- continue;
201
- }
202
- const record = decoded, now = Date.now();
203
- if (record.deadline <= now) {
204
- options.engine.storage.delete(key);
205
- continue;
206
- }
207
- const remaining = record.deadline - now;
208
- context.registry.assignTask(record.workerId, record.operationId, remaining, void 0, record.attemptToken);
209
- context.deadlineTracker.add({ operationId: record.operationId, deadline: record.deadline });
210
- const tracked = context.registry.getWorkerTasks(record.workerId).find((t) => t.operationId === record.operationId);
211
- if (tracked)
212
- tracked.visibilityTimeout = record.visibilityTimeout;
213
- rebuildWorkflowIndex(context, record.operationId, record.workflowId);
214
- }
215
- }, "restore in-flight tasks from storage").catch((error) => {
216
- console.error("[weft] Failed to restore in-flight tasks from storage:", error);
217
- });
218
- }
219
- function rebuildWorkflowIndex(context, operationId, workflowId) {
220
- if (!workflowId)
221
- return;
222
- let opIds = context.workflowOperations.get(workflowId);
223
- if (!opIds) {
224
- opIds = new Set;
225
- context.workflowOperations.set(workflowId, opIds);
226
- }
227
- opIds.add(operationId);
228
- context.operationToWorkflow.set(operationId, workflowId);
229
- }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Type guards and encode/decode for the durable remote task ledger (WFT-25).
3
+ *
4
+ * Every stored or wire-derived field is proven from `unknown` — the project
5
+ * brief's Security section requires operation IDs, workflow IDs, names,
6
+ * queues, header counts, header bytes, retry fields, and payload sizes to be
7
+ * bounded before a storage write, and `input`/`headers` are treated as
8
+ * hostile the same way `worker/manifest/parse.ts` treats manifest content.
9
+ *
10
+ * Guards here intentionally check only the fields that are load-bearing for
11
+ * correctness and the fields the brief calls out for bounding — matching the
12
+ * existing `isQueuedRecord`/`isInflightRecord` style in `task-state.ts`
13
+ * rather than re-deriving every field's shape from the type system.
14
+ *
15
+ * @module server/task-ledger-codec
16
+ */
17
+ import type { RemoteTaskCancelling, RemoteTaskCompleting, RemoteTaskDeadLettered, RemoteTaskLeased, RemoteTaskQueued, RemoteTaskRecord, RemoteTaskTerminal, RemoteTaskTerminalCancelled, RemoteTaskTerminalResolved, RemoteTaskTerminalRetryExhausted } from './task-ledger-types.ts';
18
+ /** Bounded header map: at most {@link MAX_TASK_HEADER_COUNT} entries, each key/value at most {@link MAX_TASK_HEADER_VALUE_BYTES}. */
19
+ export declare function isValidTaskHeaders(value: unknown): value is Readonly<Record<string, string>>;
20
+ export declare function isRemoteTaskQueued(value: unknown): value is RemoteTaskQueued;
21
+ export declare function isRemoteTaskLeased(value: unknown): value is RemoteTaskLeased;
22
+ export declare function isRemoteTaskCompleting(value: unknown): value is RemoteTaskCompleting;
23
+ export declare function isRemoteTaskCancelling(value: unknown): value is RemoteTaskCancelling;
24
+ export declare function isRemoteTaskTerminalResolved(value: unknown): value is RemoteTaskTerminalResolved;
25
+ export declare function isRemoteTaskTerminalCancelled(value: unknown): value is RemoteTaskTerminalCancelled;
26
+ export declare function isRemoteTaskTerminalRetryExhausted(value: unknown): value is RemoteTaskTerminalRetryExhausted;
27
+ export declare function isRemoteTaskTerminal(value: unknown): value is RemoteTaskTerminal;
28
+ export declare function isRemoteTaskDeadLettered(value: unknown): value is RemoteTaskDeadLettered;
29
+ /** Discriminate and validate a decoded value as any {@link RemoteTaskRecord} state. */
30
+ export declare function isRemoteTaskRecord(value: unknown): value is RemoteTaskRecord;
31
+ /** Canonical encoding for a validated {@link RemoteTaskRecord}. */
32
+ export declare function encodeRemoteTaskRecord(record: RemoteTaskRecord): Uint8Array;
33
+ /**
34
+ * Decode and validate a task ledger record. Returns `null` when the bytes are
35
+ * absent or decode to a value that fails bounds validation. This does not
36
+ * catch `decode()` throwing on bytes that are not valid MessagePack; storage
37
+ * bytes are always ones this codebase wrote, so byte-level corruption is
38
+ * treated as a storage integrity failure worth surfacing, not swallowing.
39
+ */
40
+ export declare function decodeRemoteTaskRecord(bytes: Uint8Array | null): RemoteTaskRecord | null;